Sweep only the logs whose writer is gone, and read a make recipe past the silence prefix that hid a removal from the gate
This commit is contained in:
parent
022d953a03
commit
0632a3086c
4 changed files with 139 additions and 3 deletions
|
|
@ -131,8 +131,26 @@ conditions:
|
|||
# the one that matters: it covers every OTHER way a log can go missing (a failed redirect, a full
|
||||
# disk, a hand), and it turns «nothing to read» into a failure instead of a clean bill. ⚠ The two are
|
||||
# not redundant — the unique name removes today's cause, the guard removes the CLASS.
|
||||
#
|
||||
# ⛔ AND THE SWEEP IS THE PRICE OF THE PER-RUN NAME, not tidiness. The fixed name limited itself: a
|
||||
# failed run left ONE stale file and the next run wrote over it. Per-run names removed that property
|
||||
# along with the collision, so every failed or interrupted run leaves its log behind for good — four
|
||||
# of them accumulated in one night. They are gitignored, so nothing reaches a commit; what rots is
|
||||
# READABILITY, and precisely where it is load-bearing: this recipe KEEPS the log on failure on
|
||||
# purpose, and a heap of debris from interrupted runs is indistinguishable from the one file somebody
|
||||
# was told to read.
|
||||
#
|
||||
# ⚠ IT SWEEPS ONLY LOGS WHOSE WRITER IS GONE (`kill -0`, which is POSIX and needs no `/proc`), and
|
||||
# that condition is the whole of it: a bare `rm -f .check.log.*` would delete a CONCURRENT run's log
|
||||
# and restore the very defect the per-run name was introduced to remove. A recycled PID keeps a stale
|
||||
# file — the conservative direction, and the only one that cannot destroy evidence. ⚠ Consequence
|
||||
# worth knowing rather than discovering: the log kept by a FAILED run is swept by the NEXT run, so
|
||||
# read it before re-running.
|
||||
check: build vet fmt lint sqlc-check
|
||||
@log=.check.log.$$$$; \
|
||||
@for f in .check.log.*; do [ -e "$$f" ] || continue; p=$${f##*.}; \
|
||||
case "$$p" in ''|*[!0-9]*) continue;; esac; \
|
||||
kill -0 "$$p" 2>/dev/null || rm -f "$$f"; done; \
|
||||
log=.check.log.$$$$; \
|
||||
$(GO) test ./... -race -count=1 -v > "$$log" 2>&1; status=$$?; \
|
||||
if [ ! -s "$$log" ]; then \
|
||||
echo "--- THE BATTERY LEFT NO LOG ($$log): every line below would be a grep over a file that is not there ---"; \
|
||||
|
|
|
|||
|
|
@ -143,6 +143,7 @@
|
|||
| PD-422 | bug | info | `internal/runs/runs.go:408`=`resnapshot := book.BankMoved || book.HasPriorRun`, `internal/pgstore/books.go:1048`=`HasPriorRun bool` | **`--resnapshot` платформа передаёт УСЛОВНО, а условие ставит только ПРАВКА банка — рост авто-банка от майнинга его не ставит.** Флаг выводится под `if book.BankMoved`, а единственный писатель `bank_moved_at` — дверь правок банка. На книге, которая МАЙНИТ банк, вторая покупка без правок банка идёт без флага, и движковый джоб-гард останавливает прогон (`exit 1` ⇒ `failed` на стороне платформы): авто-банк растёт от покупки к покупке, edit-снапшот съезжает, а гард банк-онли-движение от смены конфига не отличает. ⚠ Сегодня БЕСПРЕДМЕТНО: проводка `tmctl translate --max-units` на платформе гейчена оркестратором до лечения, а без неё вторая покупка этой формы не возникает. Строка заведена, чтобы условность не всплыла сюрпризом при снятии гейта. Найдено бэкенд-сессией `textmachine-e4` (пак «деньги»), проверено чтением платформенной стороны сессией P11 ⚠ **БЕСПРЕДМЕТНОСТЬ КОНЧИЛАСЬ И ДЕФЕКТ ЗАКРЫТ ТЕМ ЖЕ ПАКОМ (05.09), статус флипает ЛЕНДИНГ.** Гейт на проводку `--max-units` снят строкой 280, флаг едет в argv — значит условность `--resnapshot` перестала быть теоретической ровно в тот момент. Условие расширено: `resnapshot := book.BankMoved || book.HasPriorRun` (`internal/runs/runs.go`, греп `book.HasPriorRun`). Довод, почему флаг на КАЖДОМ продолжении безопасен: гард срабатывает ПО ДЖОБУ, то есть только на главах, которых прогон касается, а объёмный потолок допускает НОВУЮ книгу прежде пере-делки (`backend/internal/pipeline/volume.go`, проход `unitFresh` затем `unitRework`) — продолжение тратит грант на недоставленные главы. Согласие при этом фондированное: собственный холд прогона, никогда бланкетная форма. Пин: `runs.TestASecondPurchaseCarriesResnapshotEvenWithoutABankCorrection` (первая покупка флага НЕ несёт, вторая несёт, правки банка не было). | fixed(628cc56) | бэкенд-пак «деньги» + пак P11 (сверка шва) |
|
||||
| PD-439 | standards | info | `docs/DEFECT_REGISTER.md` (строки `PD-59`, `PD-115`, `PD-122`, `PD-273`, `PD-380`, `PD-407`, `PD-44`), шапка регистра (словарь статусов), `internal/gates/register_test.go` (греп `registerStatus`) | **Семь строк несут в колонке статуса не статус, и каждая из них невидима для всякого счёта, который эту колонку читает.** Словарь шапки — `open` · `fixed(<commit>)` · `accepted-risk(<кем, когда>)`; в дереве встречаются `open (наблюдаемость закрыта P5; ops и конфигурация — нет)`, `open (грейс-половина закрыта D39.162)`, `open (добавление закрыто P5; удаление — нет)`, `closed (решение владельца 17.08)`, `fixed` без коммита (дважды) и `**закрыт ратификацией**, работа уходит строкой 103`. Зонный `awk` сверяет ячейку с `open` ТОЧНО, поэтому три аннотированных открытых ряда не попадают ни в одно число, которое зона печатала (включая «open=96»), а `fixed` без коммита нарушает правило «закрытие — только с коммитом фикса». ⚠ Найдено новым гейтом класса: он читает статус по словарю с аннотацией, считает такие ряды открытыми, называет нечитаемые поимённо и краснеет, если нечитаемый ряд стоит под заголовком «Открытые» и несёт маркеры тревоги. Строки НЕ правлю: смена статуса — акт лендинга, а здесь под вопросом и форма, и содержание вердикта | open | самопроверка пака P13 (гейт класса, первый прогон) |
|
||||
| PD-457 | bug | info | `sqlc.yaml` (`overrides`), колонка `books.source_chars` | **У NULLABLE `bigint` НЕТ ПОДСТАНОВКИ ТИПА, ХОТЯ КОНФИГ ОБЕЩАЕТ УКАЗАТЕЛЬ ДЛЯ NULLABLE-ЦЕЛЫХ.** Миграция 00033 делает `books.source_chars` нуллабельной намеренно («движок не сказал» — не «ноль»), а `sqlc` для неё подстановки не имеет. Сегодня не ломает ничего: рукописный код читает колонку как `*int64` и конвертирует сам, `sqlc diff` чист. ⚠ Риск ОТЛОЖЕННЫЙ, а не отсутствующий, и наступит на ПЕРВОМ запросе `queries/*.sql`, который её выберет, — скан NULL в не-указатель. **Тот же класс, что и денежные колонки** (строка бэклога 304, заведена оркестратором), но другая колонка и другая подстановка: та про `*_micro_usd`, эта про размер текста. ⚠ Названо в отчёте пака (пункт ⑵) без носителя вне него | open | второй круг пака «форма заказа», 06.09 |
|
||||
| PD-458 | bug | info | `Makefile` цель `check`, подметание в первом шаге; гейт `internal/gates.TestTheBatterySweepsOnlyLogsWhoseWriterIsGone` | **ПОЧИНКА `PD-454` ЗАБРАЛА СВОЙСТВО, КОТОРОГО НИКТО НЕ НАЗЫВАЛ: ЛОГИ СТАЛИ НАКАПЛИВАТЬСЯ.** Прежнее ФИКСИРОВАННОЕ имя лога само себя ограничивало — сколько бы прогонов ни падало, оставался ОДИН несвежий файл, и следующий его перезаписывал. Попрогонное имя убрало коллизию ВМЕСТЕ с этим ограничением: каждый упавший или прерванный прогон оставляет свой `.check.log.<pid>` навсегда. ⚠ Замер, а не опасение: за одну ночь накопилось **четыре** (все нулевого размера, от прерванных прогонов зоны). В коммит не попадают (`.gitignore` = `.check.log*`), и цена НЕ в этом: рецепт НАРОЧНО сохраняет лог при падении, чтобы его прочли, и куча мусора от прерванных прогонов от этого одного файла НЕОТЛИЧИМА. То есть гниёт ровно та читаемость, ради которой лог и сохраняется. ⇒ подметание перед прогоном, и оно **условно по живости писателя** (`kill -0` — POSIX, без `/proc`): голое `rm -f .check.log.*` снесло бы лог ПАРАЛЛЕЛЬНОГО прогона и вернуло бы ровно ту коллизию, ради устранения которой заводилось попрогонное имя. Перезанятый PID оставляет файл — консервативное направление, единственное, что не уничтожает улику. Нечисловой хвост пропускается. ⚠ Следствие, названное, а не оставленное на открытие: лог, сохранённый УПАВШИМ прогоном, подметается СЛЕДУЮЩИМ — читать до пере-запуска. Три мутации ловятся: голое `rm -f .check.log.*` · цикл без проверки живости · подметание убрано совсем. ⚠ Найдено ЗОНОЙ при ответе на вопрос оркестратора «остались ли сомнения» — то есть вопросом, а не гейтом; ни один сторож второго порядка у починки не стоял | open | зона, 06.09, при закрытии смены |
|
||||
|
||||
## Принятый риск
|
||||
|
||||
|
|
|
|||
|
|
@ -401,6 +401,38 @@ makes the book parsed, and a book that is parsed owes a tree»). ⇒ окно «
|
|||
Доказано было ОТОБРАЖЕНИЕ, сказано — про ЖИЗНЬ. Цитируя пин в утверждении о значении, спрашивать не
|
||||
«зелен ли он», а «то ли он мерит».
|
||||
|
||||
### ⛔ ЧЕТВЁРТЫЙ АКТ ПО ПРИБОРУ — ПОЧИНКА ЗАБРАЛА СВОЙСТВО, КОТОРОГО НИКТО НЕ НАЗЫВАЛ (`PD-458`)
|
||||
|
||||
⚠ **Класс — MINOR, и раздувать его до крита я не стала.** Файлы нулевые, в коммит не попадают, ни
|
||||
денег, ни данных, ни лжи на проводе. Но чинится тремя строками, а откладывать трёхстрочную правку
|
||||
церемонией — это и есть «экономия усилия на неудобном»; владелец назвал это прямо, и он прав.
|
||||
|
||||
**ЧТО ЗАБРАЛА ПОЧИНКА `PD-454`.** Фиксированное имя лога само себя ограничивало: сколько бы прогонов
|
||||
ни падало, оставался ОДИН несвежий файл, следующий его перезаписывал. Попрогонное имя убрало коллизию
|
||||
ВМЕСТЕ с этим ограничением. Замер, а не опасение: за ночь накопилось ЧЕТЫРЕ файла. ⇒ гниёт ровно та
|
||||
читаемость, ради которой лог и сохраняется: рецепт НАРОЧНО держит лог упавшего прогона, чтобы его
|
||||
прочли, и куча мусора от прерванных прогонов от него неотличима.
|
||||
|
||||
**ПОДМЕТАНИЕ УСЛОВНО ПО ЖИВОСТИ ПИСАТЕЛЯ** (`kill -0` — POSIX, без `/proc`, так что линуксовая
|
||||
оговорка, которую я назвала первой редакцией, снята). Голое `rm -f .check.log.*` снесло бы лог
|
||||
ПАРАЛЛЕЛЬНОГО прогона и вернуло бы ту самую коллизию. Перезанятый PID оставляет файл — консервативное
|
||||
направление, единственное, что не уничтожает улику. Проверено исполнением: живой лог уцелел, мёртвый
|
||||
снят, нечисловой хвост пропущен.
|
||||
|
||||
**ГЕЙТ `TestTheBatterySweepsOnlyLogsWhoseWriterIsGone`** держит форму «удалять можно СВОЙ лог и ничей
|
||||
больше»; три мутации ловятся — голое `rm -f`, цикл без проверки живости, подметание убрано совсем.
|
||||
|
||||
⛔ **И ДВА СОБСТВЕННЫХ ДЕФЕКТА ЭТОГО ЖЕ ГЕЙТА, пойманных прогоном, а не чтением:**
|
||||
1. **Третий раз за смену — слово вместо команды.** Проверка `strings.Contains(s, "rm ")` сработала на
|
||||
`echo`, где «ala**rm m**arkers». Соседний гейт предупреждает об этом капкане ПРОЗОЙ, я в него вошла
|
||||
в третий раз. ⇒ вынесено в НАЗВАННУЮ функцию `removesAFile`, через которую обязана идти любая такая
|
||||
проверка: прозаические предупреждения этот класс явно не останавливают, у функции есть шанс.
|
||||
2. ⛔ **Мутация покраснела ВЕРНОЙ строкой по НЕВЕРНОЙ причине.** `@rm -f .check.log.*` — make клеит
|
||||
префикс тишины к первой команде рецепта, первый токен `@rm`, и детектор её не увидел; гейт сказал
|
||||
«никогда не удаляет старый лог» о рецепте, который первым же действием удалял их ВСЕ. Вердикт был
|
||||
бы засчитан как «мутация поймана». **Правый вердикт по неправой причине — дыра, а не пропуск**, и
|
||||
отличается он от настоящего только тем, что кто-то прочёл ТЕКСТ сообщения, а не его цвет.
|
||||
|
||||
### ⛔ ТО ЖЕ РАСЩЕПЛЕНИЕ В СОСЕДНИХ ПОЛЯХ — ПРОВЕРЕНО МЕХАНИЧЕСКИ, НЕ ГЛАЗАМИ
|
||||
|
||||
Заказ оркестратора: «проверь заодно, нет ли того же расщепления в соседних полях». Сделан сканер по
|
||||
|
|
@ -838,8 +870,10 @@ ordered_units: integer|null // НОВОЕ: сколько юнитов ку
|
|||
make check → MAKE-EXIT=0 ⟵ ПЕРЕ-СНЯТО 06.09 ПОСЛЕ ДОФИКСА ПО КАНОНУ, красных НЕТ
|
||||
golangci-lint: 0 issues · gofmt чист · go vet чист · sqlc diff чист
|
||||
ПАКЕТОВ 20: ok 20, FAIL 0
|
||||
ТЕСТОВ верхнеуровневых 854: PASS 849 · FAIL 0 · SKIP 5
|
||||
РЕГИСТР: 457 рядов · открытых 105 · major 3 · minor 35 · info 67
|
||||
ТЕСТОВ верхнеуровневых 855: PASS 850 · FAIL 0 · SKIP 5
|
||||
РЕГИСТР: 458 рядов · открытых 106 · major 3 · minor 35 · info 68
|
||||
⚠ Подметание проверено СКВОЗНО: два посаженных лога мёртвых PID перед прогоном,
|
||||
«ЛОГОВ ПОСЛЕ: 0» — рецепт снял их сам, своего не оставил.
|
||||
(847-й — гейт прибора `TestTheBatteryCannotReportCleanlinessWithoutItsLog`; 848-й —
|
||||
выделенный пин провенанса `TestABooksCutProvenanceIsTheEnginesOwnWordOrAnExplicitNull`;
|
||||
849-й — пин ЖИЗНЕННОГО состояния `pgstore.TestAParsedBookIsNotStartedBeforeItHasAnyProvenance`,
|
||||
|
|
|
|||
|
|
@ -513,3 +513,86 @@ func TestTheBatteryCannotReportCleanlinessWithoutItsLog(t *testing.T) {
|
|||
"EXIT CODE a machine reads — and the landing reads the code", code)
|
||||
}
|
||||
}
|
||||
|
||||
// ⛔ THE BATTERY MAY DELETE ITS OWN LOG AND NOBODY ELSE'S.
|
||||
//
|
||||
// The per-run name that removed the collision took a property with it: the fixed name limited itself
|
||||
// to ONE stale file, and per-run names accumulate one per failed or interrupted run. The recipe
|
||||
// sweeps them — and a sweep is exactly where the original defect can walk back in, because
|
||||
// `rm -f .check.log.*` is the obvious way to write it and it deletes a CONCURRENT run's evidence
|
||||
// mid-recipe, which is the collision the per-run name exists to prevent.
|
||||
//
|
||||
// So the shape held here is: any removal aimed at a log OTHER than this run's own must carry a
|
||||
// liveness test in the same step. `kill -0` is what the recipe uses — POSIX, no `/proc` — and a
|
||||
// recycled PID keeps a stale file, which is the conservative direction and the only one that cannot
|
||||
// destroy evidence.
|
||||
//
|
||||
// Mutation caught: replacing the conditional sweep with a bare `rm -f .check.log.*`; dropping the
|
||||
// liveness test while keeping the loop; sweeping after the run has started rather than before.
|
||||
func TestTheBatterySweepsOnlyLogsWhoseWriterIsGone(t *testing.T) {
|
||||
steps := []string{}
|
||||
for _, cmd := range makeRecipe(t, "check") {
|
||||
for _, s := range strings.Split(cmd, ";") {
|
||||
steps = append(steps, strings.TrimSpace(s))
|
||||
}
|
||||
}
|
||||
logRef := ""
|
||||
for _, s := range steps {
|
||||
if strings.Contains(s, "test ./...") {
|
||||
if _, after, found := strings.Cut(s, "> "); found {
|
||||
logRef = strings.Trim(strings.Fields(after)[0], `"`)
|
||||
}
|
||||
}
|
||||
}
|
||||
if logRef == "" {
|
||||
t.Fatal("the `check` recipe does not redirect its test run to a file this gate can find")
|
||||
}
|
||||
// A removal of THIS run's own log is the ordinary cleanup and needs no liveness test: nothing else
|
||||
// can be writing it. Every OTHER removal is aimed at somebody else's file.
|
||||
swept := false
|
||||
for _, s := range steps {
|
||||
if !removesAFile(s) || strings.Contains(s, logRef) {
|
||||
continue
|
||||
}
|
||||
swept = true
|
||||
if !strings.Contains(s, "kill -0") {
|
||||
t.Errorf("the `check` recipe removes a log that is not its own without asking whether its "+
|
||||
"writer is still alive:\n\t%s\nA sweep that does not test liveness deletes a concurrent "+
|
||||
"run's evidence mid-recipe — the collision the per-run name was introduced to remove, "+
|
||||
"walking back in through the tidy-up", s)
|
||||
}
|
||||
}
|
||||
if !swept {
|
||||
t.Error("the `check` recipe never removes an OLD log. Per-run names do not clean up after " +
|
||||
"themselves: every failed or interrupted run leaves its file behind, and the heap becomes " +
|
||||
"indistinguishable from the one log a failing run deliberately keeps for a reader")
|
||||
}
|
||||
}
|
||||
|
||||
// removesAFile says whether a recipe step INVOKES `rm`, as opposed to containing those two letters.
|
||||
//
|
||||
// ⚠ Written as a function after a substring test matched «ala`rm m`arkers» inside an echo and blamed
|
||||
// the recipe for a defect it did not have. That is the THIRD time in one shift that a check of this
|
||||
// file confused a word with a command — the sibling gate warns about it in prose, the guard's own
|
||||
// echo tripped the exit check, and now this. Prose warnings evidently do not stop it; a named
|
||||
// function that every such check must go through has a chance to.
|
||||
func removesAFile(step string) bool {
|
||||
// ⚠ MAKE'S SILENCE PREFIX IS GLUED TO THE FIRST COMMAND OF A RECIPE (`@rm -f …`), so the first
|
||||
// token is `@rm` and not `rm`. Found by a mutation that this gate DID catch — but through the
|
||||
// wrong assertion, reporting «never removes an old log» about a recipe whose first act was
|
||||
// removing them all. A right verdict for a wrong reason is a hole, not a pass.
|
||||
fields := strings.Fields(strings.TrimPrefix(strings.TrimSpace(step), "@"))
|
||||
for i, f := range fields {
|
||||
if f != "rm" {
|
||||
continue
|
||||
}
|
||||
if i == 0 {
|
||||
return true
|
||||
}
|
||||
switch fields[i-1] {
|
||||
case "||", "&&", "then", "else", "do", "{":
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue