diff --git a/backend/internal/store/kill9_test.go b/backend/internal/store/kill9_test.go index 3090d99c..e946dcb9 100644 --- a/backend/internal/store/kill9_test.go +++ b/backend/internal/store/kill9_test.go @@ -1,7 +1,9 @@ package store import ( + "bufio" "fmt" + "io" "math" "math/rand" "os" @@ -12,16 +14,51 @@ import ( "time" ) -// kill9_test.go — Р6/Phase 0 acceptance test: kill -9 mid-work loses -// at most one in-flight call and NEVER desyncs the money from the -// checkpoints. A real SIGKILL against a subprocess (the standard library's -// helper-process pattern), not emulation. +// kill9_test.go — Р6/Phase 0 acceptance test: kill -9 mid-work loses at most one in-flight call and +// NEVER desyncs the money from the checkpoints. A real SIGKILL against a subprocess (the standard +// library's helper-process pattern), not emulation. // -// Invariant after any kill: committed_usd == SUM(checkpoints.cost_usd) -// — because settle and the checkpoint are written in ONE transaction. Orphaned -// reserved_usd are cleared by the recovery pass on open. +// Invariant after any kill: committed_usd == SUM(checkpoints.cost_usd) — settle and the checkpoint are +// written in ONE transaction. Orphaned reserved_usd are cleared by the recovery pass on open. +// +// The helper announces its first committed checkpoint on stdout and the parent kills it only once that +// line has arrived. That handshake is what gives the assertions their subject: the kill is known to land +// on a process that has already committed, so a checkpoint the helper CONFIRMED before dying has to be +// on disk when the database is reopened. That is durability, not merely internal consistency, and it is +// the stronger of the two claims this test can make. Liveness is a fact the helper reports rather than +// an inference from a clock, so a helper that never commits is a loud failure instead of a round that +// quietly asserts nothing. +// +// The database is shared across rounds, so every stored figure is cumulative and only the INCREMENT +// speaks about the round that just ran: a total is non-zero for the rest of the run once any round has +// written, whatever the helper does afterwards. Each round therefore compares against the previous +// round's own reading and requires strict growth, which is also why no round needs an exemption. +// +// Two shapes this test deliberately does not take: +// - a line per commit would fill the 64 KB pipe and stall the helper, leaving the test measuring its +// own plumbing. The helper prints exactly once, and the parent drains to EOF regardless so nothing +// the testing framework writes later can block either. +// - polling the database while the helper runs would block on the exclusive flock store.Open takes. +// Nothing polls: the previous round's reading is this round's baseline, and no second connection is +// ever opened against a live helper. -const killLoopEnv = "TM_STORE_KILL_LOOP_DB" +const ( + killLoopEnv = "TM_STORE_KILL_LOOP_DB" + // killLoopRoundEnv scopes the helper's request hashes to its round. Checkpoints are idempotent by + // request_hash (ON CONFLICT DO NOTHING), so two rounds that generated the same hash would settle + // once between them, leave the count flat, and make the increment check below report a durability + // failure that never happened. The round makes each round's writes unambiguously its own. + killLoopRoundEnv = "TM_STORE_KILL_LOOP_ROUND" + // killLoopReady is printed by the helper exactly once, after its FIRST settle+checkpoint has + // committed. It is the whole liveness signal: everything the parent asserts afterwards is about work + // that is known to be on disk. + killLoopReady = "KILL9-HELPER-COMMITTED-FIRST-CHECKPOINT" + // killLoopReadyWait bounds the wait for that signal. It is deliberately far larger than any plausible + // first-commit latency (the race detector's ~15× on this loop included): it is not a performance + // budget, it is the difference between "the helper is slow" and "the helper is not committing", and + // only the second is a failure. + killLoopReadyWait = 60 * time.Second +) func TestHelperKillLoop(t *testing.T) { path := os.Getenv(killLoopEnv) @@ -40,6 +77,7 @@ func TestHelperKillLoop(t *testing.T) { if err != nil { os.Exit(1) } + round := os.Getenv(killLoopRoundEnv) // Infinite reserve → settle+checkpoint loop; the parent will SIGKILL us // at a random moment. No Release — recovery will clear the "crashed" reservations. for i := 0; ; i++ { @@ -48,7 +86,7 @@ func TestHelperKillLoop(t *testing.T) { os.Exit(1) } cp := Checkpoint{ - RequestHash: fmt.Sprintf("hash-%d-%d", os.Getpid(), i), + RequestHash: fmt.Sprintf("hash-%s-%d-%d", round, os.Getpid(), i), JobID: job.ID, ChunkIdx: i, Stage: "draft", Role: "translator", ModelRequested: "m", ModelActual: "m", ResponseText: "ответ", UsageJSON: "{}", CostUSD: 0.001, @@ -56,31 +94,108 @@ func TestHelperKillLoop(t *testing.T) { if err := s.SettleWithCheckpoint(res, 0.001, cp, nil); err != nil { os.Exit(1) } + if i == 0 { + // ONCE, and only after the write is durable. Announcing before the settle would put the + // parent's whole assertion back on a guess. + fmt.Println(killLoopReady) + } } } +// killHelper is a running helper plus the one piece of bookkeeping its lifecycle needs: os/exec is +// explicit that "it is incorrect to call Wait before all reads from the pipe have completed", because +// Wait closes the pipe out from under the reader. drained is closed when the reader has reached EOF, +// and killAndReap is the only place Wait is called. +type killHelper struct { + cmd *exec.Cmd + drained <-chan struct{} +} + +// killAndReap SIGKILLs the helper and collects it in the order os/exec requires. +func (h *killHelper) killAndReap(t *testing.T, round int) { + t.Helper() + if err := h.cmd.Process.Signal(syscall.SIGKILL); err != nil { + t.Fatalf("round %d: SIGKILL: %v", round, err) + } + <-h.drained // every read from the pipe has completed… + _ = h.cmd.Wait() // …and only now may Wait close it +} + +// startKillHelper launches the helper and returns once it has COMMITTED its first checkpoint, so the +// caller knows the kill it is about to send will land on a process that has already done real work. +// Everything it can go wrong with is a loud failure, never a skip and never a silent pass. +func startKillHelper(t *testing.T, round int, dbPath string) *killHelper { + t.Helper() + exe, err := os.Executable() + if err != nil { + t.Fatal(err) + } + cmd := exec.Command(exe, "-test.run", "^TestHelperKillLoop$", "-test.v") + cmd.Env = append(os.Environ(), killLoopEnv+"="+dbPath, fmt.Sprintf("%s=%d", killLoopRoundEnv, round)) + stdout, err := cmd.StdoutPipe() + if err != nil { + t.Fatalf("round %d: helper stdout pipe: %v", round, err) + } + if err := cmd.Start(); err != nil { + t.Fatalf("round %d: start helper: %v", round, err) + } + // One reader for the whole life of the process: it reports the marker and then keeps draining, so the + // pipe can never fill and stall the helper mid-loop. It reports reaching EOF without the marker as an + // error rather than as silence, because "the helper never committed" and "the helper died" are + // different failures and a test that confuses them sends the next reader down the wrong path. + ready := make(chan error, 1) + drained := make(chan struct{}) + go func() { + defer close(drained) + sc := bufio.NewScanner(stdout) + seen := false + for sc.Scan() { + if !seen && sc.Text() == killLoopReady { + seen = true + ready <- nil + } + } + if !seen { + ready <- fmt.Errorf("helper exited without ever committing a checkpoint (scanner: %v)", sc.Err()) + } + // An over-long line would have stopped the scanner above; keep draining regardless so a stalled + // pipe can never be mistaken for a stalled store. + _, _ = io.Copy(io.Discard, stdout) + }() + h := &killHelper{cmd: cmd, drained: drained} + select { + case err := <-ready: + if err != nil { + <-drained + _ = cmd.Wait() + t.Fatalf("round %d: %v — there was nothing for SIGKILL to land on, so everything asserted afterwards would be vacuous", round, err) + } + case <-time.After(killLoopReadyWait): + h.killAndReap(t, round) + t.Fatalf("round %d: the helper did not commit a first checkpoint within %s — the kill would have landed on a process that had done nothing, and everything asserted afterwards would be vacuous", + round, killLoopReadyWait) + } + return h +} + func TestKillMinus9LosesAtMostOneCall(t *testing.T) { if testing.Short() { t.Skip("subprocess test") } - exe, err := os.Executable() - if err != nil { - t.Fatal(err) - } path := filepath.Join(t.TempDir(), "kill9.db") + // The database is shared across rounds, so every stored figure is cumulative. The previous round's own + // reading is this round's baseline — which is also why no second connection is ever opened while the + // helper holds the project lock. + var prevCheckpoints int + var prevCommitted float64 + for round := 0; round < 5; round++ { // D23.4: 3→5 rounds — more SIGKILL windows per run - cmd := exec.Command(exe, "-test.run", "^TestHelperKillLoop$", "-test.v") - cmd.Env = append(os.Environ(), killLoopEnv+"="+path) - if err := cmd.Start(); err != nil { - t.Fatal(err) - } - // Let it run for a random amount of time, then SIGKILL — nothing graceful. - time.Sleep(time.Duration(150+rand.Intn(400)) * time.Millisecond) - if err := cmd.Process.Signal(syscall.SIGKILL); err != nil { - t.Fatal(err) - } - _ = cmd.Wait() + helper := startKillHelper(t, round, path) + // The kill lands at a random point of a reserve→settle cycle. This delay varies WHERE it lands; + // whether there is anything for it to land on was settled by the readiness signal. + time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond) + helper.killAndReap(t, round) s, err := Open(path) // the recovery pass clears the stuck reservations if err != nil { @@ -107,9 +222,19 @@ func TestKillMinus9LosesAtMostOneCall(t *testing.T) { t.Fatalf("round %d: committed=%v != sum(checkpoints)=%v over %d checkpoints — settle+checkpoint not atomic", round, committed, checkpointSum, checkpointCount) } - if checkpointCount == 0 && round > 0 { - t.Fatalf("round %d: helper made no progress", round) + // DURABILITY: the helper reported a committed checkpoint before it was killed, so this round must + // have added at least one, in every round including the first. + if checkpointCount <= prevCheckpoints { + t.Fatalf("round %d: %d checkpoints before the kill, %d after reopening — a checkpoint the helper confirmed as committed did not survive SIGKILL, or the round did no work the previous rounds had not already done", + round, prevCheckpoints, checkpointCount) } - t.Logf("round %d: %d checkpoints, committed=$%.6f — consistent after SIGKILL", round, checkpointCount, committed) + // The money must have moved with them, by the same increment logic. + if committed <= prevCommitted { + t.Fatalf("round %d: committed went from $%.6f to $%.6f across a round that added %d checkpoint(s)", + round, prevCommitted, committed, checkpointCount-prevCheckpoints) + } + t.Logf("round %d: +%d checkpoints (%d total), committed=$%.6f — a confirmed commit survived SIGKILL and the ledger matches", + round, checkpointCount-prevCheckpoints, checkpointCount, committed) + prevCheckpoints, prevCommitted = checkpointCount, committed } } diff --git a/backend/internal/store/migrate_test.go b/backend/internal/store/migrate_test.go index 03a4bb3d..725f2ced 100644 --- a/backend/internal/store/migrate_test.go +++ b/backend/internal/store/migrate_test.go @@ -397,29 +397,60 @@ func TestTheMigrationSeamRunsUnderTheLockAndOnlyWhenAStepWillRun(t *testing.T) { } } -// TestTheSeamIsNotChargedToTheStoreOperationBudget: the seam's work is the CALLER's (a restore point -// whose cost scales with the book), and opTimeout bounds operations that are O(1) in the size of the -// book. While the two shared one deadline, a large project failed the migration on the copy it had just -// paid for — «context deadline exceeded» on the first step, exit 1 outside the refusal band, and a -// retry that copied the whole database again (round-2 review, reproduced on 2.6 GB). +// seamBudget is what opTimeout is shrunk to while the migration under test runs. Under -race with every +// core busy, the three budgeted operations of a head-1 open — schemaBaseline, one applyStep, the +// recovery pass — cost about 70 ms in total on this machine, so a per-operation second leaves them some +// forty times the room they need. The seam then sleeps a multiple of it, and that sleep is the only time +// this test costs the battery. +const seamBudget = 1 * time.Second + +// TestTheSeamIsNotChargedToTheStoreOperationBudget: the seam's work is the CALLER's — a restore point +// whose cost scales with the book — while opTimeout bounds operations that are O(1) in the size of the +// book. Sharing one deadline between the two makes a large project fail its migration on the copy it has +// just paid for: «context deadline exceeded» on the first step, exit 1 outside the refusal band, and a +// retry that copies the whole database again (round-2 review, reproduced on 2.6 GB). // -// The budget is shrunk instead of the seam sleeping ten real seconds: same property, no dead time in -// the battery. It is restored on the way out and nothing here runs in parallel. +// The starting point is built BEFORE the budget is shrunk. A head-1 Open is seventeen budgeted +// operations — schemaBaseline, fifteen migration steps, the recovery pass — and not one of them is the +// subject here. Under a shrunken deadline they would be real DDL running a race they have no reason to +// be in, and on a busy machine they lose it: the same load that costs the head-1 open 70 ms costs this +// fixture up to 1.10 s, an average of 65 ms per operation. The shrink covers the migration under test +// and nothing else. +// +// The seam's elapsed time is measured and required to exceed the budget because that is this test's +// PRECONDITION rather than a consequence of it. A sleep that failed to outlive the deadline would let +// the migration succeed for the ordinary reason and report a green proving nothing, so vacuity is a +// failure here. Time is read in the one direction where reading it is safe: a slow machine can only make +// the seam longer. func TestTheSeamIsNotChargedToTheStoreOperationBudget(t *testing.T) { head := SchemaHead() - defer func(original time.Duration) { opTimeout = original }(opTimeout) - opTimeout = 150 * time.Millisecond - path := filepath.Join(t.TempDir(), "book.db") - openAtVersion(t, path, head-1) + openAtVersion(t, path, head-1) // fixture FIRST, at the full budget: 17 operations that are not the subject + defer func(original time.Duration) { opTimeout = original }(opTimeout) + opTimeout = seamBudget + + seams := 0 + var seamTook time.Duration applied, err := Migrate(path, func(Migration) error { - time.Sleep(3 * opTimeout) // a restore point on a book far larger than the budget + seams++ + start := time.Now() + time.Sleep(3 * seamBudget) // a restore point on a book far larger than the budget + seamTook = time.Since(start) return nil }) if err != nil { t.Fatalf("a slow seam must not fail the migration it protects: %v", err) } + // The precondition, checked rather than assumed: without a seam that genuinely outlived the budget + // there is no claim here, and a green result would mean nothing. + if seams != 1 { + t.Fatalf("the seam ran %d time(s), want exactly 1 — with no seam this test asserts nothing", seams) + } + if seamTook <= seamBudget { + t.Fatalf("the seam took %v, which did NOT outlive the %v operation budget — the migration "+ + "would have survived a shared deadline too, so this test proved nothing", seamTook, seamBudget) + } if applied.From != head-1 || applied.To != head { t.Fatalf("transition %+v, want v%d -> v%d", applied, head-1, head) } diff --git a/docs/ORCHESTRATOR_SESSION_PROMPT.md b/docs/ORCHESTRATOR_SESSION_PROMPT.md index 4f5e193f..0e50e7e8 100644 --- a/docs/ORCHESTRATOR_SESSION_PROMPT.md +++ b/docs/ORCHESTRATOR_SESSION_PROMPT.md @@ -135,7 +135,7 @@ D-ссылка грепается по D-логу · mid-flight аддендум | Характер | Чем проверяется | |---|---| -| Код | Исполнением: батарея + живые пробы + СОБСТВЕННЫЕ адверсариальные посадки ВНЕ списка автора; сверка с индустриальным первоисточником (stdlib/RFC/устоявшаяся библиотека прежде велосипеда); пак, меняющий отгружаемые байты или добавляющий механизм, принимается прогоном через настоящий драйвер, не чтением диффа; у шва — инвентарь ВСЕХ каналов другой стороны чтением ЧУЖОГО кода, не по памяти (D39.132) | +| Код | ⚠ **Тест утверждает не то, что думает (D39.171):** часы как способ синхронизации там, где предмет утверждения НЕ время, и ассерт по подстроке в буфере, куда пишет кто-то ещё. Обе формы дают и ложное красное, и **тихо-зелёное** — а оно дороже: зелёное, не проверившее ничего, не перечитывает никто. Исполнением: батарея + живые пробы + СОБСТВЕННЫЕ адверсариальные посадки ВНЕ списка автора; сверка с индустриальным первоисточником (stdlib/RFC/устоявшаяся библиотека прежде велосипеда); пак, меняющий отгружаемые байты или добавляющий механизм, принимается прогоном через настоящий драйвер, не чтением диффа; у шва — инвентарь ВСЕХ каналов другой стороны чтением ЧУЖОГО кода, не по памяти (D39.132) | | Полигон / замеры | Пере-выводом чисел из СЫРЬЯ; независимость сигналов + leave-one-out (сфабрикованная сходимость — типовой провал); катастроф-скрин к победителю; вывод на агрегате до вскрытия единиц запрещён (D39.61) | | Ресёрч / текст | Claim-fidelity: цитаты — дословные подстроки первоисточников (типовой провал — ОВЕР-АТРИБУЦИЯ, не фабрикация); пере-открыть выборку улик; опровергатель ДРУГОЙ моделью | | Доки | Сверка утверждений против кода и живых носителей | diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 9fcdae7f..3adf284a 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -1,6 +1,6 @@ # Журнал прогресса -> **⟶ ТЕКУЩЕЕ СОСТОЯНИЕ** (на 2026-08-29, голова D39.170 — КУРС: движок и платформа до «работает и отдаёт результат», фронт ЗАМОРОЖЕН и P7 его НЕ размораживает (D39.147). **ОЧЕРЕДЬ №19** (единственный носитель — здесь; роль передана 22.08, №18 закрыт D39.155): (а) **P7 платформы ПРИНЯТ С ФИКС-ЛИСТОМ и ЗАЛЕНДЕН, сессия закрыта владельцем 20.08** (D39.153; зонный фикс-лист из тринадцати пунктов плюс врезанный первым блокер свипа — пинг №18 в `platform/docs/platform-PROGRESS.md`; ⚠ прежний врезанный пример «первым `ContractVersion` 0.3.0 при формах 0.4.0» СНЯТ — константа поднята и запинена гейтом против канона лендингом `31f1f82`) · (а1) **контракт API v0 0.4.0 РАТИФИЦИРОВАН** (D39.152; PD-327 закрыт; зеркало фронта отстаёт ратифицированно, при разморозке `cmp`) · (б) **лендинг петель полигона (фаза Д) — ОТКРЫТ**: приёмка ФАЗЫ у параллельного оркестратора, лендинг за этим; ⚠ **предупреждение в силе: работа полигона в дереве по-прежнему НЕЗАКОММИЧЕНА** (20 позиций: `START_PROMT.MD` + 14 правленых и 5 новых файлов `eval/`). Владелец перевозил её на новую машину транспортным коммитом `temp`, но 24.08 **снял его словом владельца** (`reset --mixed`, дерево не тронуто — сверено побайтно, бэкап `~/tm-backup-2026-08-24/textmachine-HEAD.bundle` + reflog): транспортный коммит вдобавок уносил `START_PROMT.MD`, который канон коммитить запрещает. Состав читать командой `git status --short -- eval/`, снимок здесь протухает. Не трогать и не «прибирать»; приёмка ФАЗЫ — за параллельным оркестратором, пинг №20 — в секции «Полигон». Форма коммита строго pathspec · (в) **дофикс-пак P8-FIX ПРИНЯТ И ЗАЛЕНДЕН 22.08** (D39.154, лендинг `31f1f82`): блокер свипа закрыт четырьмя механизмами и пере-проверен живой пробой оркестратора, деньги сошлись двумя путями из сырого леджера; мои четыре строки регистра — `PD-371`…`PD-374`. ⚠ **Порядок вышел ОБРАТНЫМ этой очереди:** промт дофикса требовал запуска ПОСЛЕ читающего пака, запущен был раньше. Цена перестановки, названная мной вслух 21.08 («блокер живёт всё время читающего пака»), тем самым НЕ заплачена — это лучший исход, чем планировалось; в минус — §4.7 (аддендум релеем) пуст и читающий пак пойдёт по только что переписанному коду. **ЧТО ДАЛЬШЕ С ПЛАТФОРМОЙ — решено 22.08 при актуализации доков, порядок такой.** **(1) Читающий пак P8-REVIEW — ОТРАБОТАН, ПРИНЯТ И ЗАЛЕНДЁН 27.08** (D39.159): четыре оси, 24 новые строки регистра (`PD-375`…`PD-398`), 17 дописок, каталог воспроизведения `platform/docs/p8-review/`. Единственный vuln — `PD-379` (major): открытый поток событий переживает отзыв сессии. Числа сдачи пере-мерены приёмкой на дереве С лендингом бэкенда. Промт архивируется. **Следующая работа зоны — кодовый пак по этим строкам, запуск по слову владельца.** Промт ЖИВ и переписан 22.08 в трёх местах, которые дофикс сделал ложными: «два денежных дефекта не заведены» (заведены и закрыты — `PD-333`/`PD-334`, вместо них дан их КЛАСС и живой образец `PD-372`) · приор оси 4 «наблюдаемость без ручки» (ручка построена ⇒ вопрос стал «достаточна ли она») · абсолют «оси ни в один дифф не входили» (сужен: системного разбора не было, кусками входили). **Довод ЗА пак — базовая ставка, а не вера:** первый же серьёзный взгляд на ОДНУ из четырёх осей (очередь, 21.08) дал пред-продовый блокер, проживший два пака под статусом `fixed`. Осей осталось три, и на двух из них лежат деньги и вход. **(2) ПОРЯДОК РАБОТ ПО ШВУ — ратифицирован D39.156 (23.08), исполняется строго так.** **(2а) Движковый пак шва — ОТРАБОТАН И ЗАЛЕНДЖЕН 27.08 (D39.158), промт в `archive/prompts/`** (аудит доков 28.08 поймал, что очередь пять дней звала его АКТИВНЫМ и направляла сессию в исполненное). Состав, для истории: конвенционные дефолты `mined_delta`/`mined_rejects` · $0-глагол приёма правок банка · путь к файлу ключей аргументом (**211** — движковый конец; платформенный доехал паком P9, строка ЗАКРЫТА D39.162) · строгость сид-загрузчика · путь артефакта и версия документа в `status --json` (**213** — движковая половина; платформенная снята P9, у строки остался один дев-путь). **(2б) Контрактный минор — ПОСЛЕ него и ОДНИМ куском:** снос отменённой пер-термной модели + новая дверь, тело которой выводится из словаря глагола, а не сочиняется до него; сюда же строка **203** и хвосты компаньона. ⚠ Строка **200** ушла НЕ сюда: D39.160 изъял её из этого минора и отправил с паком (2в), где она и закрылась минором **0.6.0**. ⚠ Гигиена держится сознательно: снести дверь сейчас и завести в следующем миноре — две перегенерации фронта вместо одной; фолбэк объявлен — если пак застрянет надолго, гигиену шипить отдельно. **(2в) Платформенный пак P9 — ОТРАБОТАН, ПРИНЯТ И ЗАЛЕНДЁН 28.08 (D39.162):** дверь правок банка смонтирована синхронно и под пер-книжным мьютексом · ключи доехали до движка аргументом `--keys-file` (строка **211** ЗАКРЫТА) · дубль конвенции пути снят (строка **213** — осталась одна строка в дев-пути) · снята сквозная полоса прогресса вместе с её канонной половиной, минор **0.6.0** (строка **200** ЗАКРЫТА) · с провода сняты два поля упразднённой модели (`PD-399`). ⚠ Пункт «воркер решений» СНЯТ эрратой 27.08-з: синхронность держится, замер сессии её подтвердил. Фикс-раунд приёмки — 13 позиций, две мои диспозиции сессия ОПРОВЕРГЛА исполнением и я это принял (см. D39.162). **(3) `sqlc` отдельной сессией** (решение владельца 22.08, `BACKLOG.md` П-19, граница 41 запрос в пяти файлах) — механическая работа, не мешать её с содержательной. **(4) Живой прогон книги насквозь** — строка 202, гейт прежний: ПОСЛЕ холодного прогона движка. ⚠ **На владельце и блокирует пункт своей темы:** ⚠ **развилка владения `book.yaml` (199а) СНЯТА — с владельца убрана аудитом 28.08:** она растворена седьмым вариантом ещё D39.156 п.3 (конвенционные дефолты + движковый глагол), вход построен D39.158/D39.162, а вся строка 199 закрыта D39.166 и снесена с таблицы. Пять дней держалась на владельце как блокер решённого. ⚠ **ДВА ВОПРОСА, СНЯТЫЕ ВЛАДЕЛЬЦЕМ 28.08 (D39.165) — оставлены здесь как след, потому что реестр ожиданий о них не знал вовсе и это была дыра учёта.** Ответы: смена формы конвейера — форма «ЭПОХА» (событие книги, канон уже разрешает пересчёт через границу `structure_version`); единица продажи — цена от ОБЪЁМА исходника по `chapters.units_total` + потолок объёма в движке + хвост качества в тариф, калибровка гейчена строкой 202. Прежняя формулировка: (дыру учёта нашла разведка 28.08: нота сказала «ждёт слова владельца», а реестр ожиданий об этом не знал): **(i) смена формы конвейера на ЖИВОЙ книге** — что деплой обязан делать с книгой, начерченной по старой форме, когда форму сменили (носители `PD-403`/`PD-404`; ⚠ проект отвечал на СОСЕДНИЙ вопрос дважды — D39.152 п.4 и D39.153 §4б — и два ратифицированных ответа В КОМПОЗИЦИИ и дают дефект: липкость флага ратифицирована ПРОТИВ отката назад, но в обратную сторону она морозит счёт, и комментарий `platform/internal/pgstore/sink.go:405-408` описывает поведение, которого его же код НЕ делает); **(ii) `PD-410`** — платформа продаёт ГЛАВЫ, движок останавливается по ДЕНЬГАМ, и единица продажи не равна единице остановки · (г) **контракт: сквозная полоса прогресса ЗАКРЫТА** — минор **0.6.0** заленджен вместе с паком P9 (D39.162): доля одна на всю работу прогона через обе волны, подпись стадии объявлена ВТОРЫМ ограниченным исключением из границы «ничего о том, КАК переводится книга». ⚠ Остаток назван и НЕ забыт: платформа продаёт главы, а движку передаёт только `--ceiling-usd` — единицы разные, и это `PD-410` (архитектурный стоп зоны). Хвосты релеев (203) ждут следующего минора · (д) свободные бэкенд: 160 Этап 0, фикс-лист ФЧ строкой 197 (носителя-сессии нет по слову владельца); ⚠ строки 176/181/187 СНЯТЫ с таблицы при лендинге 20.08 — они закрыты ещё D39.149, а очередь всё это время звала 181 свободной работой (поймано ревью доков; остатки живут строками 194/196/197) · (е) фронтовый фрагмент pre-commit хука получает норму «жалуйся владельцу» — **решение владельца 21.08: ДА**, исполняется первым касанием зоны (пинг в её журнале). · (ж) **ПЕРЕЕЗД НА НОВУЮ МАШИНУ — стенд ПЕРЕСОБРАН 24.08 (№19), как поднимать — карта `README.md`, строки «Тулчейн» и `counts.py`.** Ubuntu 26.04 WSL2, пользователь `ubuntu-26`; тулчейн стоял НУЛЕВОЙ. Поставлено: Go 1.26.7 · golangci-lint 2.12.2 · Node 22.23.2 — тарболами вендоров со сверкой sha256 в `~/.local/opt` (симлинки в `~/.local/bin`); PostgreSQL 18.4 — рецептом самой зоны (`platform/docs/STACK_DECISIONS.md` §«Postgres на стенде без root»); `build-essential` + `python3-venv` — руками владельца, они единственные требовали root. **Обе батареи зелены под `-race`:** бэкенд `make battery` EXIT=0, линтер 0 issues, скипы только корпус-гейченные и хелперы; платформа `make check` EXIT=0, 18 пакетов, линтер 0 issues, **скипов 0**. Фронт: `npm ci` + `npm run check` проходит целиком (335 тестов), `vite build` собирается. Сквозняк проверен ЖИВЬЁМ: демон платформы → сид через настоящий интейк → движок распарсил книгу (3 главы) → дев-прокси фронта достучался до платформы. ⚠ **Что НЕ проверено:** ни одного платного вызова · браузеров Playwright нет ⇒ `check:full` не гонялся · локальной модели и GPU на машине нет вовсе (слово владельца 24.08: мерить нечем, ветка не закрыта) · OIDC не поднимался, вход только дев-логином. ⚠ Правка `.gitignore` владельца несла дефект — правило `books/` без якоря глушило ЛЮБОЙ каталог `books`, включая живой пакет `platform/internal/books/` (новый файл там уходил бы в игнор молча); заякорено в `/books/`, проверено пробой в обе стороны. Новые строки бэклога — **217** (пути полигона), **218** (дефолты корпуса в движке — ЗАКРЫТА и снята 27.08, D39.159 эррата-д), **219** (line-якоря D-лога съехали — ⚠ ОБЪЯВЛЕНА 24.08, а В ТАБЛИЦЕ ЗАВЕДЕНА только 27.08: три дня очередь ссылалась на несуществующий носитель) **Открытые обязательства, которых нет больше нигде:** веса `pro` — строка 172-г НЕ закрыта, прогон мерил классификатор на flash; дешёвый вход — спросить полигон, говорят ли что-то данные фазы Д (там pro гоняется редактором), отдельный замер не покупать без этого · `docs/experiments/00-provider-quirks.md` несёт протухшие цены DeepSeek («не изменились, flash $0.14/$0.28») — зона полигона, чинить пингом · санкция на платный прогон в архивном промте пака честности была РАЗОВОЙ · **норма плотности комментариев — ОТВЕЧЕНА владельцем 21.08:** режем ВОДУ, а не длину — счёт строк «пиши одну-две» негодный гейт и снят; уходит пересказ решений, провенанс и изложение исследования вместо ссылки, остаётся всё, что из одной функции не выводится, сколько бы строк ни заняло (норма переписана — `12-go-style-notes` §1; PD-255 платформы этим закрывается). ⚠ Метод-урок: её хендофф объявил «вопросов на владельце нет», а строка регистра просила именно решения — вопрос был закрыт ОБЪЯВЛЕНИЕМ, поймано ревьюером полноты выгрузки. Оркестраторов ДВА (решение владельца 07.08): этот — движок/платформа/фронт/доки; параллельный (РОЛЬЮ, без номера — счётчик один, D39.112 п.6) — приёмка полигона. Одновременно не запускаются; CURRENT-STATE ведут оба, чужие строки не трогают. Норма изоляции панелей — D39.113, гардрейлы в CLAUDE.md.) +> **⟶ ТЕКУЩЕЕ СОСТОЯНИЕ** (на 2026-08-29, голова D39.171 — КУРС: движок и платформа до «работает и отдаёт результат», фронт ЗАМОРОЖЕН и P7 его НЕ размораживает (D39.147). **ОЧЕРЕДЬ №19** (единственный носитель — здесь; роль передана 22.08, №18 закрыт D39.155): (а) **P7 платформы ПРИНЯТ С ФИКС-ЛИСТОМ и ЗАЛЕНДЕН, сессия закрыта владельцем 20.08** (D39.153; зонный фикс-лист из тринадцати пунктов плюс врезанный первым блокер свипа — пинг №18 в `platform/docs/platform-PROGRESS.md`; ⚠ прежний врезанный пример «первым `ContractVersion` 0.3.0 при формах 0.4.0» СНЯТ — константа поднята и запинена гейтом против канона лендингом `31f1f82`) · (а1) **контракт API v0 0.4.0 РАТИФИЦИРОВАН** (D39.152; PD-327 закрыт; зеркало фронта отстаёт ратифицированно, при разморозке `cmp`) · (б) **лендинг петель полигона (фаза Д) — ОТКРЫТ**: приёмка ФАЗЫ у параллельного оркестратора, лендинг за этим; ⚠ **предупреждение в силе: работа полигона в дереве по-прежнему НЕЗАКОММИЧЕНА** (20 позиций: `START_PROMT.MD` + 14 правленых и 5 новых файлов `eval/`). Владелец перевозил её на новую машину транспортным коммитом `temp`, но 24.08 **снял его словом владельца** (`reset --mixed`, дерево не тронуто — сверено побайтно, бэкап `~/tm-backup-2026-08-24/textmachine-HEAD.bundle` + reflog): транспортный коммит вдобавок уносил `START_PROMT.MD`, который канон коммитить запрещает. Состав читать командой `git status --short -- eval/`, снимок здесь протухает. Не трогать и не «прибирать»; приёмка ФАЗЫ — за параллельным оркестратором, пинг №20 — в секции «Полигон». Форма коммита строго pathspec · (в) **дофикс-пак P8-FIX ПРИНЯТ И ЗАЛЕНДЕН 22.08** (D39.154, лендинг `31f1f82`): блокер свипа закрыт четырьмя механизмами и пере-проверен живой пробой оркестратора, деньги сошлись двумя путями из сырого леджера; мои четыре строки регистра — `PD-371`…`PD-374`. ⚠ **Порядок вышел ОБРАТНЫМ этой очереди:** промт дофикса требовал запуска ПОСЛЕ читающего пака, запущен был раньше. Цена перестановки, названная мной вслух 21.08 («блокер живёт всё время читающего пака»), тем самым НЕ заплачена — это лучший исход, чем планировалось; в минус — §4.7 (аддендум релеем) пуст и читающий пак пойдёт по только что переписанному коду. **ЧТО ДАЛЬШЕ С ПЛАТФОРМОЙ — решено 22.08 при актуализации доков, порядок такой.** **(1) Читающий пак P8-REVIEW — ОТРАБОТАН, ПРИНЯТ И ЗАЛЕНДЁН 27.08** (D39.159): четыре оси, 24 новые строки регистра (`PD-375`…`PD-398`), 17 дописок, каталог воспроизведения `platform/docs/p8-review/`. Единственный vuln — `PD-379` (major): открытый поток событий переживает отзыв сессии. Числа сдачи пере-мерены приёмкой на дереве С лендингом бэкенда. Промт архивируется. **Следующая работа зоны — кодовый пак по этим строкам, запуск по слову владельца.** Промт ЖИВ и переписан 22.08 в трёх местах, которые дофикс сделал ложными: «два денежных дефекта не заведены» (заведены и закрыты — `PD-333`/`PD-334`, вместо них дан их КЛАСС и живой образец `PD-372`) · приор оси 4 «наблюдаемость без ручки» (ручка построена ⇒ вопрос стал «достаточна ли она») · абсолют «оси ни в один дифф не входили» (сужен: системного разбора не было, кусками входили). **Довод ЗА пак — базовая ставка, а не вера:** первый же серьёзный взгляд на ОДНУ из четырёх осей (очередь, 21.08) дал пред-продовый блокер, проживший два пака под статусом `fixed`. Осей осталось три, и на двух из них лежат деньги и вход. **(2) ПОРЯДОК РАБОТ ПО ШВУ — ратифицирован D39.156 (23.08), исполняется строго так.** **(2а) Движковый пак шва — ОТРАБОТАН И ЗАЛЕНДЖЕН 27.08 (D39.158), промт в `archive/prompts/`** (аудит доков 28.08 поймал, что очередь пять дней звала его АКТИВНЫМ и направляла сессию в исполненное). Состав, для истории: конвенционные дефолты `mined_delta`/`mined_rejects` · $0-глагол приёма правок банка · путь к файлу ключей аргументом (**211** — движковый конец; платформенный доехал паком P9, строка ЗАКРЫТА D39.162) · строгость сид-загрузчика · путь артефакта и версия документа в `status --json` (**213** — движковая половина; платформенная снята P9, у строки остался один дев-путь). **(2б) Контрактный минор — ПОСЛЕ него и ОДНИМ куском:** снос отменённой пер-термной модели + новая дверь, тело которой выводится из словаря глагола, а не сочиняется до него; сюда же строка **203** и хвосты компаньона. ⚠ Строка **200** ушла НЕ сюда: D39.160 изъял её из этого минора и отправил с паком (2в), где она и закрылась минором **0.6.0**. ⚠ Гигиена держится сознательно: снести дверь сейчас и завести в следующем миноре — две перегенерации фронта вместо одной; фолбэк объявлен — если пак застрянет надолго, гигиену шипить отдельно. **(2в) Платформенный пак P9 — ОТРАБОТАН, ПРИНЯТ И ЗАЛЕНДЁН 28.08 (D39.162):** дверь правок банка смонтирована синхронно и под пер-книжным мьютексом · ключи доехали до движка аргументом `--keys-file` (строка **211** ЗАКРЫТА) · дубль конвенции пути снят (строка **213** — осталась одна строка в дев-пути) · снята сквозная полоса прогресса вместе с её канонной половиной, минор **0.6.0** (строка **200** ЗАКРЫТА) · с провода сняты два поля упразднённой модели (`PD-399`). ⚠ Пункт «воркер решений» СНЯТ эрратой 27.08-з: синхронность держится, замер сессии её подтвердил. Фикс-раунд приёмки — 13 позиций, две мои диспозиции сессия ОПРОВЕРГЛА исполнением и я это принял (см. D39.162). **(3) `sqlc` отдельной сессией** (решение владельца 22.08, `BACKLOG.md` П-19, граница 41 запрос в пяти файлах) — механическая работа, не мешать её с содержательной. **(4) Живой прогон книги насквозь** — строка 202, гейт прежний: ПОСЛЕ холодного прогона движка. ⚠ **На владельце и блокирует пункт своей темы:** ⚠ **развилка владения `book.yaml` (199а) СНЯТА — с владельца убрана аудитом 28.08:** она растворена седьмым вариантом ещё D39.156 п.3 (конвенционные дефолты + движковый глагол), вход построен D39.158/D39.162, а вся строка 199 закрыта D39.166 и снесена с таблицы. Пять дней держалась на владельце как блокер решённого. ⚠ **ДВА ВОПРОСА, СНЯТЫЕ ВЛАДЕЛЬЦЕМ 28.08 (D39.165) — оставлены здесь как след, потому что реестр ожиданий о них не знал вовсе и это была дыра учёта.** Ответы: смена формы конвейера — форма «ЭПОХА» (событие книги, канон уже разрешает пересчёт через границу `structure_version`); единица продажи — цена от ОБЪЁМА исходника по `chapters.units_total` + потолок объёма в движке + хвост качества в тариф, калибровка гейчена строкой 202. Прежняя формулировка: (дыру учёта нашла разведка 28.08: нота сказала «ждёт слова владельца», а реестр ожиданий об этом не знал): **(i) смена формы конвейера на ЖИВОЙ книге** — что деплой обязан делать с книгой, начерченной по старой форме, когда форму сменили (носители `PD-403`/`PD-404`; ⚠ проект отвечал на СОСЕДНИЙ вопрос дважды — D39.152 п.4 и D39.153 §4б — и два ратифицированных ответа В КОМПОЗИЦИИ и дают дефект: липкость флага ратифицирована ПРОТИВ отката назад, но в обратную сторону она морозит счёт, и комментарий `platform/internal/pgstore/sink.go:405-408` описывает поведение, которого его же код НЕ делает); **(ii) `PD-410`** — платформа продаёт ГЛАВЫ, движок останавливается по ДЕНЬГАМ, и единица продажи не равна единице остановки · (г) **контракт: сквозная полоса прогресса ЗАКРЫТА** — минор **0.6.0** заленджен вместе с паком P9 (D39.162): доля одна на всю работу прогона через обе волны, подпись стадии объявлена ВТОРЫМ ограниченным исключением из границы «ничего о том, КАК переводится книга». ⚠ Остаток назван и НЕ забыт: платформа продаёт главы, а движку передаёт только `--ceiling-usd` — единицы разные, и это `PD-410` (архитектурный стоп зоны). Хвосты релеев (203) ждут следующего минора · (д) свободные бэкенд: 160 Этап 0, фикс-лист ФЧ строкой 197 (носителя-сессии нет по слову владельца); ⚠ строки 176/181/187 СНЯТЫ с таблицы при лендинге 20.08 — они закрыты ещё D39.149, а очередь всё это время звала 181 свободной работой (поймано ревью доков; остатки живут строками 194/196/197) · (е) фронтовый фрагмент pre-commit хука получает норму «жалуйся владельцу» — **решение владельца 21.08: ДА**, исполняется первым касанием зоны (пинг в её журнале). · (ж) **ПЕРЕЕЗД НА НОВУЮ МАШИНУ — стенд ПЕРЕСОБРАН 24.08 (№19), как поднимать — карта `README.md`, строки «Тулчейн» и `counts.py`.** Ubuntu 26.04 WSL2, пользователь `ubuntu-26`; тулчейн стоял НУЛЕВОЙ. Поставлено: Go 1.26.7 · golangci-lint 2.12.2 · Node 22.23.2 — тарболами вендоров со сверкой sha256 в `~/.local/opt` (симлинки в `~/.local/bin`); PostgreSQL 18.4 — рецептом самой зоны (`platform/docs/STACK_DECISIONS.md` §«Postgres на стенде без root»); `build-essential` + `python3-venv` — руками владельца, они единственные требовали root. **Обе батареи зелены под `-race`:** бэкенд `make battery` EXIT=0, линтер 0 issues, скипы только корпус-гейченные и хелперы; платформа `make check` EXIT=0, 18 пакетов, линтер 0 issues, **скипов 0**. Фронт: `npm ci` + `npm run check` проходит целиком (335 тестов), `vite build` собирается. Сквозняк проверен ЖИВЬЁМ: демон платформы → сид через настоящий интейк → движок распарсил книгу (3 главы) → дев-прокси фронта достучался до платформы. ⚠ **Что НЕ проверено:** ни одного платного вызова · браузеров Playwright нет ⇒ `check:full` не гонялся · локальной модели и GPU на машине нет вовсе (слово владельца 24.08: мерить нечем, ветка не закрыта) · OIDC не поднимался, вход только дев-логином. ⚠ Правка `.gitignore` владельца несла дефект — правило `books/` без якоря глушило ЛЮБОЙ каталог `books`, включая живой пакет `platform/internal/books/` (новый файл там уходил бы в игнор молча); заякорено в `/books/`, проверено пробой в обе стороны. Новые строки бэклога — **217** (пути полигона), **218** (дефолты корпуса в движке — ЗАКРЫТА и снята 27.08, D39.159 эррата-д), **219** (line-якоря D-лога съехали — ⚠ ОБЪЯВЛЕНА 24.08, а В ТАБЛИЦЕ ЗАВЕДЕНА только 27.08: три дня очередь ссылалась на несуществующий носитель) **Открытые обязательства, которых нет больше нигде:** веса `pro` — строка 172-г НЕ закрыта, прогон мерил классификатор на flash; дешёвый вход — спросить полигон, говорят ли что-то данные фазы Д (там pro гоняется редактором), отдельный замер не покупать без этого · `docs/experiments/00-provider-quirks.md` несёт протухшие цены DeepSeek («не изменились, flash $0.14/$0.28») — зона полигона, чинить пингом · санкция на платный прогон в архивном промте пака честности была РАЗОВОЙ · **норма плотности комментариев — ОТВЕЧЕНА владельцем 21.08:** режем ВОДУ, а не длину — счёт строк «пиши одну-две» негодный гейт и снят; уходит пересказ решений, провенанс и изложение исследования вместо ссылки, остаётся всё, что из одной функции не выводится, сколько бы строк ни заняло (норма переписана — `12-go-style-notes` §1; PD-255 платформы этим закрывается). ⚠ Метод-урок: её хендофф объявил «вопросов на владельце нет», а строка регистра просила именно решения — вопрос был закрыт ОБЪЯВЛЕНИЕМ, поймано ревьюером полноты выгрузки. Оркестраторов ДВА (решение владельца 07.08): этот — движок/платформа/фронт/доки; параллельный (РОЛЬЮ, без номера — счётчик один, D39.112 п.6) — приёмка полигона. Одновременно не запускаются; CURRENT-STATE ведут оба, чужие строки не трогают. Норма изоляции панелей — D39.113, гардрейлы в CLAUDE.md.) > - ⚠ **ОБЯЗАТЕЛЬСТВА ЛЕНДИНГА P9, часть 2 (28.08):** перевести половину 2 строки `PD-400` (внутрипроцессный мьютекс) в статус **`accepted-risk`** — граница v1 названа с условием («перестаёт держать в день второй реплики; лечение тогда арбитр в хранилище, не больший мьютекс») и ценой (ограничена убираемым шумом: холостая попытка у translate, «повтори позже» у глагола — ни ложного слова, ни денег); однорепличность — сегодняшний допуск всей зоны. Смена статуса — акт лендинга, строка сессией под это уже сформулирована. ⚠ Там же закрыть `PD-401` — лечение в дереве и зелёное. > - ⚠ **ОБЯЗАТЕЛЬСТВО ЛЕНДИНГА P9, записано 27.08, чтобы не потерялось второй раз:** при взятии дерева закрыть **`PD-166`** — её тело уже объявляет механизм НЕДОСТИЖИМЫМ (запись версии переехала из `Begin` в `effect`, одна транзакция с курсором; `Begin` — legacy), диспозиция «закрыть как построенное» написана, пин `TestTheChunkerVersionOfTheStreamReachesTheBook` (`pgstore/sink_test.go:618`) на месте, согласие получено ещё до рестарта — **не исполнена только смена статуса**. Проверка одной командой: `awk -F'|' '/^\| PD-166 \|/ {print $7}' platform/docs/DEFECT_REGISTER.md` → ` open `. Закрытие — акт лендинга, не работа пака (прецедент P6). ⚠ Сейчас регистр правит сессия P9 — параллельно не лезть. > - ⚠ **ЛЕНТА КОНТРАКТА за 27–28.08, свёрнуто аудитом 28.08 (D39.167):** **0.5.0** — D39.161 (снос отменённой пер-термной модели подписи + дверь правок банка) · **0.6.0** — D39.162/D39.163 (сквозная полоса прогресса; `Progress.stage` ВТОРЫМ и последним исключением границы) · **0.7.0** — D39.166 (покупка пере-прохода членом `RunRequest.re_pass`). Прожитые дампы приёмки P9 и P10 вынесены в `archive/PROGRESS-2026-08-backend.md` и в тела нот; здесь остаётся только лента версий. diff --git a/docs/architecture/05-decisions-index.md b/docs/architecture/05-decisions-index.md index b7eafdab..808b2ebd 100644 --- a/docs/architecture/05-decisions-index.md +++ b/docs/architecture/05-decisions-index.md @@ -1,4 +1,4 @@ -# Реестр D-нот — карта актуальности v2 (D1–D39.170; +# Реестр D-нот — карта актуальности v2 (D1–D39.171; > ⚠ **СЛАБОЕ МЕСТО, КОТОРОЕ БЫЛО ЗДЕСЬ (вписано 22.08, ЗАКРЫТО 24.08 — D39.157 п.6).** Колонка ТЕЛА > у нот D39.107…D39.123 говорила «жив», хотя тела уехали в слайс подрезкой D39.139; семнадцать строк @@ -231,3 +231,4 @@ | D39.168 | 28.08 | **Аудит доков, часть 2 — архитектура и ресёрчи.** Две НОРМЫ требовали построенного (закон шва: «`status --json` версию не несёт — закрыть», а несёт); доки врали про `accepts_labels` («пусты у всех» при трёх заполненных эндпоинтах — ось аккаунта); денежный вход цитировал грант $5 при коде 0. Вынесены в `archive/research/` четыре мёртвых отчёта (290 КБ) по графу входящих ссылок; двум ресёрчам без шапки вовсе поставлены ревю-шапки. | жив | ЖИВОЕ: системный класс line-якорей в архитектуре; часть 3 не пройдена | доки ресёрчи архив аудит | | D39.169 | 29.08 | **Платформенный пак P11 принят и заленджен + контрактный минор 0.8.0.** Отзыв сессии гасит открытый SSE-поток (`PD-379`, единственная уязвимость `major`) и объявляет причину кадром `session_ended`, а не молчаливым обрывом в `401`; застрявший расчёт видим и закрываем; денежная группа `PD-384/391/394/397/376`. Второй рубеж сессии поймал СЕМЬ ложных подтверждений в её же отчёте (включая выдуманную мутационную посадку) — код цел, достоверность рассказа нет. Охотник вне карты нашёл ЧЕТЫРЕ регресса, введённых самим паком, все закрыты до сдачи. Две мои ошибки в каноне: схема нового кадра была сиротой вне союза `anyOf`, перечень кадров соединения противоречил телу схемы. Линтер якорей нашёл 21 указатель на исчезнувший код — норма: гонять его ПОСЛЕДНИМ шагом, после того как код замер. | жив | ЖИВОЕ: `PD-425` — открытый денежный major, отсрочка на оркестраторе; `PD-424`/`PD-426` отложены с доводом; `PD-423` — условие батареи, которого не знал рецепт | платформа контракт деньги сессии приёмка | | D39.170 | 29.08 | **Движковый пак «деньги» принят и заленджен**: потолок ОБЪЁМА оплаченной работы (`--max-units`) + читающий путь `status` теперь СВОРАЧИВАЕТ банк, поэтому смета пере-прохода впервые доезжает до покупателя ДО покупки, оставаясь $0. ⚠ Пак СНЯЛ ПОСЫЛКУ чужой зоны: платформа не берёт `rebill_*` по доводу «status читает ноль сразу после apply» (`ingest/resync.go:37-43`) — довод устарел. Четыре круга приёмки; блокирующая находка охотника: потолок ПРОБИВАЛСЯ пере-сидом банка посреди прогона (грант 1 → 4 вызова, $0.0073 вместо $0.0036, две доставленные главы оплачены дважды и названы бесплатными). Лечение структурное — пере-план + отказ волны работать с планом чужого снапшота. Сессия не воспроизвела сценарий ПЯТЬ раз при верных прогонах: дельту двигает не текст, а предложения черновиков, а `spoilerBlocked` (`membank/memory.go:640-647`) режет термин с поздним `since_ch` навсегда. | жив | ЖИВОЕ: ось отгрузки на `once_key` — отдельный заказ; терминолог вне потолка — вход в калибровку цены; проводка `--max-units` ГЕЙЧЕНА | движок деньги потолок приёмка шов | +| D39.171 | 29.08 | **Гейт денежной безопасности при `kill -9` четверть раундов проверял ТАВТОЛОГИЮ.** `kill9_test.go` сторожил живость счётчиком по ВСЕЙ базе, а путь создаётся вне цикла раундов ⇒ страж пуст в КАЖДОМ раунде; на пустой базе `committed == sum` выполняется как `0 == 0`. Замер: 10 раундов из 40 не утверждали ничего. Доказано сравнением двух форм под ОДНОЙ посадкой: старый тест PASS при четырёх пустых раундах, новый FAIL на первом. Второй тест ужимал бюджет ДО построения фикстуры (17 чужих операций под усечением). Оба починены структурно (рукопожатие + сверка прироста; усечение только на предмет), боевой код не тронут. **Норма: тест не имеет права утверждать о ВРЕМЕНИ, если предмет утверждения — не время; тихо-зелёное дороже красного.** | жив | ЖИВОЕ: две замеренные формы дефектного теста; узость класса проверяется по формам и зоне, а не вообще | тесты гейты движок приёмка | diff --git a/docs/architecture/05-decisions-log.md b/docs/architecture/05-decisions-log.md index a00617f1..4accecd1 100644 --- a/docs/architecture/05-decisions-log.md +++ b/docs/architecture/05-decisions-log.md @@ -1,4 +1,4 @@ -# Журнал решений оркестратора — контракт D1–D39.170 (живой файл: карта · эрраты · живые тела · голова D39.124+ (подрезка D39.139); тела закрытых эр — в слайсах `docs/archive/architecture/`, указатель ниже; реестр всех нот — `05-decisions-index.md`) +# Журнал решений оркестратора — контракт D1–D39.171 (живой файл: карта · эрраты · живые тела · голова 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 живёт ниже в этом файле (голова D39.106+). @@ -1332,3 +1332,65 @@ $0.007280 вместо максимум $0.003640, две уже доставл бесполезно), именованный кап — по тому, что заплатит ЭТОТ прогон (законная работа не отклоняется), прогон без пере-оплаты гейта не встречает. Первая редакция отозвана самой сессией и помечена отозванной, а не переписана молча. + +## D39.171 — ГЕЙТ ДЕНЕЖНОЙ БЕЗОПАСНОСТИ ПРИ `kill -9` ЧЕТВЕРТЬ РАУНДОВ ПРОВЕРЯЛ ТАВТОЛОГИЮ: два дефектных теста движка починены, норма «тихо-зелёное дороже красного» ратифицирована (29.08, оркестратор №19). ✅ + +**Что заленджено.** Два теста `backend/internal/store/`; **боевой код не тронут ни строкой** — показаний против движка нет ни у кого, падавшие раунды печатали согласованность, проверка резерваций не срабатывала ни разу. + +⚠ **ГЛАВНОЕ: дефект был не в том, что тест иногда краснел, а в том, что он зеленел, не проверив ничего.** +`kill9_test.go` сторожил живость условием `checkpointCount == 0 && round > 0` по счётчику ВСЕЙ базы, +а путь к базе создаётся ОДИН раз вне цикла раундов. Значит после первого коммита счётчик уже никогда +не ноль — и страж пуст в КАЖДОМ раунде, а не только в нулевом, как предполагал заказ. Замер сессии: +8 прогонов × 5 раундов под `-race` с восемью busy-loop — **10 раундов из 40 не утверждали ничего** +(нулевой во всех восьми прогонах плюс два с нулевым приростом, прошедшие молча). Денежный инвариант +`committed == sum(checkpoints)` на пустой базе выполняется тавтологически как `0 == 0`. + +**ДОКАЗАНО СРАВНЕНИЕМ ДВУХ ФОРМ ПОД ОДНОЙ ПОСАДКОЙ (моя проверка, не отчёт сессии).** Посадил хелпера, +который со второго раунда не делает ни одного коммита. **Старый тест: `PASS`**, печатая «26 checkpoints» +в раундах 0–4 — четыре раунда не сделали ничего, и он отрапортовал «consistent after SIGKILL». +**Новый: `FAIL` на первом же**, с диагнозом «a checkpoint the helper confirmed as committed did not +survive SIGKILL, or the round did no work the previous rounds had not already done». + +**Форма лечения — структурная, по входному условию владельца «не хакать, а рефакторить».** Снятие +`&& round > 0` и подъём таймаута отвергнуты как лечение симптома. Построено рукопожатие: хелпер печатает +ОДНУ строку после первого успешного `SettleWithCheckpoint`, родитель ждёт её с громким падением по +таймауту, и только затем шлёт `SIGKILL`; сверяется ПРИРОСТ за раунд. `round > 0` исчез не как снятая +проверка, а как артефакт кумулятивного счётчика. ⚠ **Тест стал СТРОЖЕ:** коммит подтверждён ДО смерти +процесса, поэтому прирост после переоткрытия — проверка ДОЛГОВЕЧНОСТИ, которой старая форма не делала +вовсе. Две ловушки реализации обойдены явно: строка на каждый коммит забила бы недренируемый пайп на +64 КБ, а `Open()` из родителя взял бы эксклюзивный `flock` и заблокировался. + +**Второй тест: дефект оказался НЕ в утверждении, а в фикстуре.** `TestTheSeamIsNotChargedToThe` +`StoreOperationBudget` ужимал `opTimeout` до 150 мс ПЕРВОЙ строкой, до `openAtVersion`, — а открытие +head-1 это семнадцать бюджетируемых операций, ни одна из которых не предмет теста. Под `-race` с +нагрузкой фикстура стоит до 1.10 с. Подпись подтверждает: старый падал на `migration 14`, шаге +ФИКСТУРЫ, при том что предмет — шаг 16. Починка: фикстура строится под полным бюджетом, усечение +накрывает только миграцию под тестом (три операции вместо двадцати). ⚠ И тест приобрёл то, чего не +утверждал: его ПРЕДПОСЫЛКА (шов реально пережил бюджет) бралась из константы сна — теперь время шва +меряется и обязано превысить бюджет, то есть вакуумная зелень стала падением, а медленная машина может +проверку только усилить. Мой прогон после починки: **8 из 8 под `-race` с восемью busy-loop**. + +**Сессия нашла в СВОЁМ коде две вещи адверсариальным проходом, обе настоящие:** `cmd.Wait()` закрывал +трубу, пока горутина ещё читала (os/exec прямо называет это некорректным) — введён `killAndReap`, +ждущий EOF; и `request_hash` хелпера строился из PID, так что совпадение PID между раундами сделало бы +`settle` пустышкой через `ON CONFLICT DO NOTHING` — и её же проверка прироста соврала бы про потерю +долговечности. Хеш скоуплен раундом. + +**РАТИФИЦИРОВАННАЯ НОРМА (общая, за пределами этих тестов).** **Тест не имеет права утверждать что-либо +о времени, если предмет утверждения — не время.** В обоих случаях предмет — атомарность и учёт, а часы +попали туда как способ синхронизации и стали слабым местом. Две ЗАМЕРЕННЫЕ формы такого теста: +(1) часы как синхронизация — дают и ложное красное, и **тихо-зелёное**, когда подготовка не успела; +(2) ассерт, ищущий значение ПОДСТРОКОЙ в буфере, куда пишет кто-то ещё — признак дешёвый: сверка идёт +с текстом, а не с полем (живой случай — `355d204`, где наносекундный таймстемп сталкивался с искомой +суммой). ⚠ **Тихо-зелёное дороже красного:** красное раздражает и потому чинится, а зелёное, не +проверившее ничего, не перечитывает никто. + +⚠ **Узость класса проверяется ПО ФОРМАМ и ПО ЗОНЕ, которые реально обошли.** Замер сессии по +`backend/**`: семь `time.Sleep` в тестах, ровно один ужатый таймаут, ровно одна конструкция «ассерт под +условием, которое при провале подготовки не выполняется» — то есть оба случая и есть почти весь класс. +Но «форм X в зоне Y почти нет» ≠ «обвязка здорова»: вторая форма приехала в дерево из ДРУГОЙ зоны в тот +же день. Сессия сузила собственный вердикт сама, до того как я успел записать его шире. + +**Цена расхождения замеров названа:** мои первые прогоны шли БЕЗ `-race`, и 2764 чекпоинта против 26–68 +под флагом — разница инструмента, а не хоста. Вывод «не воспроизвёл» был получен не тем инструментом, и +это тот самый третий исход, который нельзя сваливать во второй.