From 6a6ce973a9b2f6e04b32722a7798c5fbaf13c0f8 Mon Sep 17 00:00:00 2001 From: heaven Date: Sun, 30 Aug 2026 01:05:18 +0300 Subject: [PATCH] Land the engine's schema gate: every statement is now compiled against the schema its own migration chain builds, instead of against whichever paths the tests happen to run --- backend/internal/store/sqlgate_test.go | 575 ++++++++++++++++++++++++ docs/PROGRESS.md | 5 +- docs/architecture/05-decisions-index.md | 3 +- docs/architecture/05-decisions-log.md | 46 +- 4 files changed, 624 insertions(+), 5 deletions(-) create mode 100644 backend/internal/store/sqlgate_test.go diff --git a/backend/internal/store/sqlgate_test.go b/backend/internal/store/sqlgate_test.go new file mode 100644 index 00000000..09b46350 --- /dev/null +++ b/backend/internal/store/sqlgate_test.go @@ -0,0 +1,575 @@ +package store + +import ( + "database/sql" + "fmt" + "go/ast" + "go/parser" + "go/token" + "path/filepath" + "slices" + "strconv" + "strings" + "testing" +) + +// sqlgate: every SQL statement this package can execute is COMPILED BY SQLITE against the schema the +// migration chain builds, on every battery run. +// +// The class it closes is "the migration and the query drifted apart" — a column renamed in a migration +// while the SQL that reads it was not, which SQLite reports only when something executes that path. A +// behavioural test finds it only if it happens to run that statement, so the coverage of this class was +// previously the coverage of the test suite, which is not the same thing and nobody was tracking the +// difference. Here every statement is compiled whether or not any test reaches it. +// +// The schema is raised by running the product's OWN migration chain into a temporary file, so the gate +// checks against exactly what a real project converges to rather than against a copy of the DDL that +// could itself drift. That also makes the chain self-checking: a malformed migration fails this test +// before any statement is looked at. Nothing external is required — SQLite's schema is a file, so the +// gate holds on a clean machine with no stand, no environment and no manual setup. +// +// Compilation rather than execution: db.Prepare resolves every table, column and function name against +// the schema and reports what is missing, while leaving `?` parameters unbound. Executing instead would +// force the gate to invent values, and inventing them is how a gate starts asserting about types nobody +// wrote down — and here it would also mean writing to the database it is inspecting. +// +// WHAT IT DOES NOT PROVE. That a statement is the RIGHT statement: a query naming only existing columns +// passes here however wrong its logic, its joins or its ordering. That the values bound to `?` have the +// right types — preparation leaves them unknown on purpose. That a statement's result is scanned into +// the right Go fields, which is the ordinary business of the behavioural tests. And it says nothing +// about paths a query takes at RUNTIME beyond the variants folded below: a statement this gate cannot +// read is reported rather than skipped, so the boundary of what it sees is asserted, not assumed. +func TestEverySQLStatementPreparesAgainstTheMigratedSchema(t *testing.T) { + stmts := collectSQL(t) + // A floor on the extractor itself. Without it, an extractor that quietly stopped finding call sites + // would turn this gate into a green light over nothing. The number is the measured population minus + // a small margin for ordinary churn: 70 statements resolve today, and a drop below 65 is a collapse + // rather than an edit. + if len(stmts) < 65 { + t.Fatalf("the extractor resolved %d statements: it has stopped seeing this package, and a gate that checks nothing passes", len(stmts)) + } + db := migratedSchema(t) + for _, st := range stmts { + if err := compiles(db, st.sql); err != nil { + t.Errorf("%s: SQLite cannot compile this statement against the migrated schema: %v\n%s", + st.where, err, indentSQL(st.sql)) + } + } + t.Logf("%d statements compiled against the schema at v%d", len(stmts), SchemaHead()) +} + +// compiles hands one statement to SQLite for compilation and throws the result away. The prepared +// statement is released here rather than at the end of the run: the population is the whole package, +// and holding every one of them open would make the gate's own footprint grow with the thing it reads. +func compiles(db *sql.DB, query string) error { + stmt, err := db.Prepare(query) + if err != nil { + return err + } + defer stmt.Close() + return nil +} + +// migratedSchema builds a project database with the product's own chain and hands back a connection to +// it. Open is used rather than a hand-rolled sequence of CREATEs precisely so that the thing under +// inspection is the schema the engine really produces. +func migratedSchema(t *testing.T) *sql.DB { + t.Helper() + path := filepath.Join(t.TempDir(), "schema.db") + s, err := Open(path) + if err != nil { + t.Fatalf("building the schema to check against: %v", err) + } + t.Cleanup(func() { s.Close() }) + // The migration chain is executed here as whole multi-statement scripts, which is why its own SQL is + // not compiled statement by statement below: running it IS the check, and a chain that cannot build + // its schema fails the line above. Asserting the version closes the gap between "Open returned" and + // "the chain actually ran to head". + var v int + if err := s.r.QueryRow(`SELECT COALESCE(MAX(version), 0) FROM schema_version`).Scan(&v); err != nil { + t.Fatalf("reading the schema version of the database this gate checks against: %v", err) + } + if v != SchemaHead() { + t.Fatalf("the gate's database is at v%d, the binary's head is v%d: the statements below would be checked against a schema no run of this build produces", v, SchemaHead()) + } + return s.r +} + +// statement is one SQL string this package can execute, and where it comes from. +type statement struct { + where string + sql string +} + +// sqlCalls maps a call whose argument is SQL to the index of that argument. queryAll is the package's +// own read helper; it is listed rather than excused, because ten statements reach the driver through it +// and a gate that skipped them would skip most of the read models. +// +// Prepare and PrepareContext carry no SQL in this package today. They are listed anyway, because the +// point of the map is to name EVERY database/sql entry point through which SQL can reach the driver: an +// unlisted one is not a failure, it is silence, and the floor below cannot see a statement that was +// never counted in the first place. +var sqlCalls = map[string]int{ + "ExecContext": 1, "QueryContext": 1, "QueryRowContext": 1, "PrepareContext": 1, + "Exec": 0, "Query": 0, "QueryRow": 0, "Prepare": 0, + "queryAll": 1, +} + +// sqlHelpers are the functions in this package that take SQL as a PARAMETER. Their bodies execute +// whatever a caller hands them, so the body is not a site of its own — the call sites are. Skipped +// bodies are counted, and the count is asserted, so the skip cannot become the hole it exists to avoid. +var sqlHelpers = map[string]bool{"queryAll": true} + +// unresolvable is the call site whose SQL genuinely cannot be folded from source, with the shape it +// takes at runtime. It is a TABLE and not a skip: an entry has to be written by hand, so a second such +// site is somebody's deliberate decision rather than a hole that opened quietly. +// +// BackupSQLite builds `VACUUM INTO ''` around a path chosen at runtime. The path is data, not +// schema — the statement names no table and no column — so what the entry preserves is the only part a +// schema gate can speak about: that the statement itself still compiles. +// +// ⚠ Keyed by the enclosing FUNCTION, not by a line number: a line number moves whenever somebody edits +// the file above it, and a different statement would then inherit the entry and be checked against text +// that is not its own. An entry covers exactly ONE site. +// +// ⚠ An entry is ANCHORED to its function's source: some string literal of that function must appear in +// the entry. A hand-written string is the one thing here that cannot drift with the code it describes, +// and without the anchor a statement could be rewritten while the gate went on compiling the old text +// and reporting it green. +var unresolvable = map[string]string{ + "BackupSQLite": `VACUUM INTO '/tmp/tm-sqlgate-representative.db'`, +} + +// migrationExecutors are the functions whose SQL is the migration chain itself. They are named here, and +// their presence is asserted, because their argument is a whole multi-statement script: compiling it as +// one statement is not possible, and it needs no compiling — migratedSchema runs the chain, so a chain +// that does not build fails this test before a single query is read. +var migrationExecutors = map[string]bool{"applyStep": true} + +// collectSQL folds every SQL string in the package's non-test source. +// +// Folding rather than executing: the statements are literals, named constants, concatenations, one +// table name ranging over a literal list and one query assembled by conditional appends. All of it is +// decided at compile time, which is why a static reader can see all of it while a runtime one would see +// only what the tests happened to run. +func collectSQL(t *testing.T) []statement { + t.Helper() + sources, err := filepath.Glob("*.go") + if err != nil { + t.Fatal(err) + } + usedException := map[string]int{} + usedMigrationExecutor := map[string]int{} + t.Cleanup(func() { + // Every entry of the exception table has to be USED. An entry for a function that no longer has + // unfoldable SQL is an exception standing over a statement nobody checks any more. + for name := range unresolvable { + if usedException[name] == 0 { + t.Errorf("`unresolvable` still excuses %s, which no longer needs it: the exception now covers nothing and hides whatever moves under it", name) + } + } + for name := range migrationExecutors { + if usedMigrationExecutor[name] == 0 { + t.Errorf("`migrationExecutors` still names %s, which no longer executes the chain: the schema it stands for is now checked by nothing", name) + } + } + }) + fset := token.NewFileSet() + var files []*ast.File + for _, path := range sources { + if strings.HasSuffix(path, "_test.go") { + continue + } + f, err := parser.ParseFile(fset, path, nil, 0) + if err != nil { + t.Fatalf("%s: %v", path, err) + } + files = append(files, f) + } + // ⚠ TOP-LEVEL declarations only. Walking a whole file would descend into every function body, where + // short names like `q` belong to a dozen different statements, and a query could then be checked + // against another query's text and pass. + pkgConst := map[string]ast.Expr{} + for _, f := range files { + for _, decl := range f.Decls { + if g, ok := decl.(*ast.GenDecl); ok { + collectConsts(g, pkgConst) + } + } + } + var out []statement + helperCalls := map[string]int{} + helperBodies := 0 + for _, f := range files { + for _, decl := range f.Decls { + fn, ok := decl.(*ast.FuncDecl) + if !ok || fn.Body == nil { + continue + } + if sqlHelpers[fn.Name.Name] { + helperBodies++ + continue + } + if migrationExecutors[fn.Name.Name] { + usedMigrationExecutor[fn.Name.Name]++ + continue + } + // A name declared inside the function wins over a package-level one of the same name, so + // the narrow scope is consulted first — short names like `q` are reused across this + // package, and resolving one against another file's constant would check the wrong text. + scope := map[string]ast.Expr{} + collectConsts(fn, scope) + env := &foldEnv{ + scopes: []map[string]ast.Expr{scope, pkgConst}, + ranged: rangeStrings(fn), + appended: appendedStrings(t, fset, fn), + } + ast.Inspect(fn, func(n ast.Node) bool { + call, ok := n.(*ast.CallExpr) + if !ok { + return true + } + name := calleeName(call) + at, ok := sqlCalls[name] + if !ok || at >= len(call.Args) { + return true + } + if sqlHelpers[name] { + helperCalls[name]++ + } + where := shortPos(fset, call.Pos()) + variants, ok := env.foldAll(call.Args[at]) + if !ok { + if known, listed := unresolvable[fn.Name.Name]; listed { + if !anchoredInSource(fn, known) { + t.Errorf("%s: the entry in `unresolvable` for %s no longer shares any literal with the function's source, so it describes a statement that is no longer there", + where, fn.Name.Name) + return true + } + usedException[fn.Name.Name]++ + if usedException[fn.Name.Name] > 1 { + t.Errorf("%s: %s holds a second statement this gate cannot read, and the entry in `unresolvable` describes the first: checking it against that string would prove nothing about it", + where, fn.Name.Name) + return true + } + out = append(out, statement{where: where, sql: known}) + return true + } + // NOT skipped. A statement this gate cannot read is a statement nothing checks, and + // the whole point is that there is no such thing by accident. + t.Errorf("%s: the SQL handed to %s cannot be folded from source, so nothing checks it against the schema; make it a constant expression, or list it in `unresolvable` with the string it takes", + where, name) + return true + } + for i, sql := range variants { + at := where + if len(variants) > 1 { + at = fmt.Sprintf("%s#%d", where, i+1) + } + out = append(out, statement{where: at, sql: sql}) + } + return true + }) + } + } + for name := range sqlHelpers { + if helperCalls[name] == 0 { + t.Errorf("no call site of %s was found, and its body is skipped: the statements it executes are checked by nothing", name) + } + } + if helperBodies == 0 { + t.Error("no helper body was skipped: this gate no longer matches the package it reads") + } + slices.SortFunc(out, func(a, b statement) int { return strings.Compare(a.where, b.where) }) + return out +} + +// foldEnv is everything needed to turn one function's SQL expressions into the set of strings they can +// take: named constants in scope, loop variables bound to literal lists, and locally assembled strings. +type foldEnv struct { + scopes []map[string]ast.Expr + ranged map[string][]string + appended map[string][]string +} + +// foldAll returns every string an expression can evaluate to. A set rather than a single value, because +// a statement built around a loop variable or a conditional append is genuinely several statements, and +// checking one of them would leave the others as unguarded as before this gate existed. +func (e *foldEnv) foldAll(x ast.Expr) ([]string, bool) { + switch v := x.(type) { + case *ast.BasicLit: + if v.Kind != token.STRING { + return nil, false + } + s, err := strconv.Unquote(v.Value) + if err != nil { + return nil, false + } + return []string{s}, true + case *ast.ParenExpr: + return e.foldAll(v.X) + case *ast.BinaryExpr: + if v.Op != token.ADD { + return nil, false + } + left, okl := e.foldAll(v.X) + right, okr := e.foldAll(v.Y) + if !okl || !okr { + return nil, false + } + var out []string + for _, l := range left { + for _, r := range right { + out = append(out, l+r) + } + } + return out, true + case *ast.Ident: + if vals, ok := e.ranged[v.Name]; ok { + return vals, true + } + if vals, ok := e.appended[v.Name]; ok { + return vals, true + } + for _, scope := range e.scopes { + if def, ok := scope[v.Name]; ok { + return e.foldAll(def) + } + } + } + return nil, false +} + +// rangeStrings binds a loop variable to the literal list it ranges over — `for _, table := range +// []string{"a", "b"}`. Every element is a statement of its own: a table renamed in a migration breaks +// exactly one of them, and a gate that folded only the first would report the other three as fine. +func rangeStrings(fn *ast.FuncDecl) map[string][]string { + out := map[string][]string{} + ast.Inspect(fn, func(n ast.Node) bool { + rng, ok := n.(*ast.RangeStmt) + if !ok || rng.Value == nil { + return true + } + name, ok := rng.Value.(*ast.Ident) + if !ok { + return true + } + lit, ok := rng.X.(*ast.CompositeLit) + if !ok { + return true + } + var vals []string + for _, elt := range lit.Elts { + bl, ok := elt.(*ast.BasicLit) + if !ok || bl.Kind != token.STRING { + return true + } + s, err := strconv.Unquote(bl.Value) + if err != nil { + return true + } + vals = append(vals, s) + } + if len(vals) == 0 { + return true + } + if prior, seen := out[name.Name]; seen && !slices.Equal(prior, vals) { + out[name.Name] = nil // two lists under one name: neither is safe to attribute to a call site + return true + } + if _, blanked := out[name.Name]; !blanked || out[name.Name] != nil { + out[name.Name] = vals + } + return true + }) + for name, vals := range out { + if vals == nil { + delete(out, name) + } + } + return out +} + +// anchoredInSource reports whether the function still contains a string literal that the hand-written +// exception is built around. Short literals are ignored: a lone quote or a format verb would anchor +// anything to anything. +func anchoredInSource(fn *ast.FuncDecl, entry string) bool { + found := false + ast.Inspect(fn, func(n ast.Node) bool { + lit, ok := n.(*ast.BasicLit) + if !ok || lit.Kind != token.STRING { + return true + } + s, err := strconv.Unquote(lit.Value) + if err != nil || len(s) < 8 { + return true + } + if strings.Contains(entry, s) { + found = true + } + return true + }) + return found +} + +// appendedStrings folds a query assembled by `q := "…"` followed by `q += "…"`, and returns every form +// the finished string can take. +// +// An append guarded by an `if` is OPTIONAL, so the variable takes two values at that point and both are +// checked: the filter clause of such a query names real columns, and it is the branch a caller reaches +// only sometimes that a gate is most useful for. Anything else that writes to the variable — an append +// inside a loop, whose repetition count is not a compile-time fact, or a value that is not foldable — +// makes the variable UNREADABLE rather than half-read, and the site is then reported by the caller. +func appendedStrings(t *testing.T, fset *token.FileSet, fn *ast.FuncDecl) map[string][]string { + t.Helper() + built := map[string][]string{} + poisoned := map[string]bool{} + defined := map[string]int{} + env := &foldEnv{scopes: []map[string]ast.Expr{{}}} + + var walk func(n ast.Node, conditional bool) + walk = func(n ast.Node, conditional bool) { + switch v := n.(type) { + case *ast.AssignStmt: + if len(v.Lhs) != 1 || len(v.Rhs) != 1 { + return + } + name, ok := v.Lhs[0].(*ast.Ident) + if !ok { + return + } + vals, folded := env.foldAll(v.Rhs[0]) + switch v.Tok { + case token.DEFINE, token.ASSIGN: + if v.Tok == token.DEFINE { + // A name declared twice in one function is a shadow or a reuse, and either way the + // fold can no longer say which declaration a given call site sees. Attributing the + // wrong string to a statement is worse than admitting the statement is unreadable. + defined[name.Name]++ + if defined[name.Name] > 1 { + poisoned[name.Name] = true + return + } + } + if !folded { + // Only a string-shaped assignment can poison: a variable that never held SQL is not + // this function's business. + if _, tracked := built[name.Name]; tracked { + poisoned[name.Name] = true + } + return + } + if conditional { + poisoned[name.Name] = true // a whole statement chosen in a branch is not an append + return + } + built[name.Name] = vals + case token.ADD_ASSIGN: + base, tracked := built[name.Name] + if !tracked { + return + } + if !folded { + poisoned[name.Name] = true + return + } + var next []string + if conditional { + next = append(next, base...) // the branch not taken + } + for _, b := range base { + for _, add := range vals { + next = append(next, b+add) + } + } + built[name.Name] = next + } + case *ast.IfStmt: + for _, stmt := range v.Body.List { + walk(stmt, true) + } + if v.Else != nil { + walk(v.Else, true) + } + return + case *ast.RangeStmt, *ast.ForStmt, *ast.SwitchStmt, *ast.TypeSwitchStmt, *ast.SelectStmt: + // A body that may run any number of times cannot be folded into a finite set of strings. + ast.Inspect(n, func(inner ast.Node) bool { + as, ok := inner.(*ast.AssignStmt) + if !ok || len(as.Lhs) != 1 { + return true + } + if id, ok := as.Lhs[0].(*ast.Ident); ok { + if _, tracked := built[id.Name]; tracked { + poisoned[id.Name] = true + } + } + return true + }) + return + case *ast.BlockStmt: + for _, stmt := range v.List { + walk(stmt, conditional) + } + return + } + } + for _, stmt := range fn.Body.List { + walk(stmt, false) + } + for name := range poisoned { + delete(built, name) + } + return built +} + +// collectConsts records every string const and var whose value is a constant expression. +func collectConsts(n ast.Node, into map[string]ast.Expr) { + ast.Inspect(n, func(n ast.Node) bool { + decl, ok := n.(*ast.GenDecl) + if !ok || (decl.Tok != token.CONST && decl.Tok != token.VAR) { + return true + } + for _, spec := range decl.Specs { + vs, ok := spec.(*ast.ValueSpec) + if !ok { + continue + } + for i, name := range vs.Names { + if i < len(vs.Values) { + into[name.Name] = vs.Values[i] + } + } + } + return true + }) +} + +// calleeName is the identifier a call names, whether it is a method, a plain function or a generic one +// written with explicit type arguments. +func calleeName(call *ast.CallExpr) string { + switch fn := call.Fun.(type) { + case *ast.SelectorExpr: + return fn.Sel.Name + case *ast.Ident: + return fn.Name + case *ast.IndexExpr: + if id, ok := fn.X.(*ast.Ident); ok { + return id.Name + } + case *ast.IndexListExpr: + if id, ok := fn.X.(*ast.Ident); ok { + return id.Name + } + } + return "" +} + +func shortPos(fset *token.FileSet, p token.Pos) string { + pos := fset.Position(p) + return fmt.Sprintf("%s:%d", filepath.Base(pos.Filename), pos.Line) +} + +func indentSQL(sql string) string { + return "\t" + strings.ReplaceAll(strings.TrimSpace(sql), "\n", "\n\t") +} diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index edfe69b2..7d2907cf 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -1,6 +1,6 @@ # Журнал прогресса -> **⟶ ТЕКУЩЕЕ СОСТОЯНИЕ** (на 2026-08-29, голова D39.172 — КУРС: движок и платформа до «работает и отдаёт результат», фронт ЗАМОРОЖЕН и 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.173 — КУРС: движок и платформа до «работает и отдаёт результат», фронт ЗАМОРОЖЕН и 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` и в тела нот; здесь остаётся только лента версий. @@ -14,7 +14,7 @@ > - **Курс:** ОБЩНОСТЬ ✅ → КАЧЕСТВО БАНКА ✅ → ПАКЕТ-ЧЕКЕРОВ ✅ (D39.59–78) → **ФРОНТ-ЭРА** (D39.81–100: зоны живые, контракт API ратифицирован) → **шов/платформа/движковые блокеры построены** (D39.106–123). Хвосты курса живут строками: 16 (полная цена холодного старта не измерена: у `coldrun-a`/`coldrun-b` нет НИ ОДНОГО редакторского вызова, хотя сама edit-волна отработала и оплачена — 58 вызовов волновым драйвером 23–25.07 на малых прогонах, плюс 648 до драйвера; ⚠ прежняя редакция писала «не гонялась НИ РАЗУ» — абсолют ложен, испр. оркестратором №19 по леджерам `~/books/gu-zhenren/**/*.db`; держит и оси голоса 13б/24) · 46 (дизайн заморожен D39.92/93, промт ждёт выдачи) · coldrun-b фаза C заморожена чекпойнтом легитимно (D39.86; эталон денег/поведения — coldrun-a, read-only); развилка 0731 решена и исполнена (D39.87/91, код `553f1a3`). > - **Горизонт (D39.62/67, освежён D39.95):** **ДОБОР ИДЕАЛА** (первым прогоном: оси голоса 24 · авто-режим · цена 16; жильцы ролей решаются ДО прогона эксп-22 — строка 149 · веса K1–K12 13а · вне-претрейн чекпоинт 55; остаток арбитража банка = рецензент спорных кластеров при ре-пробе 74 — D39.102) → ВТОРАЯ ПАРА живьём (ja→ru; преп 81) → МАСШТАБ → пилот Ф2.5 (гейт резюме-строки 80; строки 62–68, 85) → Ф3 ридер-IDE (69–71). **Стоячие:** ToS-триггер 25.10 · Ш-2 до go1.27 (⚠ + x/text Unicode 17 тем же тулчейном — строка 119, реестр §Б-108 справочника якорей) · платные прогоны разблокированы (проба провода — D39.97, конфиги 112 залендены). > - **Стек (полная карта роль→модель→конфиг→квирки — [STACK.md](STACK.md), D39.126):** draft deepseek-v4-flash thinking-ON `low` **⚠0731** → терминолог (та же модель) → editor deepseek-v4-pro БИЛИНГВ ИНТЕРИМ (топология ПОДТВЕРЖДЕНА при неразличимости жильцов — D39.117; закон-блок ОБЯЗАТЕЛЕН — строка 134; glm-5 резерв; вахта маппинга эффорта pro — §Б-108) → судья gemini (Ф2, в движке НЕ построен — строка 33); канал B Mistral+grok; ~$0.85/ранобэ (D30.4, пере-калибровка при следующем платном прогоне). ⚠ **Вендор-факты 13–15.08 (пере-пин ИСПОЛНЕН и ПРИНЯТ, D39.137):** таблица цен запинена ПИКОМ (flash 0.44/1.32 · pro 1.32/3.96/кэш-хит 0.044 за 1M; счёт шиппинг-c1 = 100% DeepSeek ⇒ ×4.2–4.4 в пике / ×2.1–2.2 в долине — замер по трём прогонам); ⚠ про эффорт pro НОСИТЕЛИ ПРОТИВОРЕЧАТ и это НЕ РЕШЕНО (испр. 23.08 №19: прежняя редакция утверждала здесь одну сторону — «эффорт стал настраиваемым low/high/max, квирк 3а устарел»): квирк-канон и `STACK.md` говорят «ручки у pro НЕТ», замера поведением после 13.08 нет ни у одной стороны, и по гардрейлу владельца такое закрывается ВЕНДОР-СВЕРКОЙ, а не выбором стороны; до неё в силе канон, разбор — `STACK.md` строка редактора; ⚠ ВЕСА pro сменились под тем же слагом (V4-Pro-0813, класс D39.61) — вахта-риг готов (остаток 172); ⚠ посылка интерим-редактора «dspro дешевле glm» в пике ПЕРЕВЁРНУТА (×1.26 дороже — вход ратификации фазы Д, D39.137 п.4); покупки фазы Д на deepseek до 16.08 16:00 UTC — по старым ценам. ⚠ **ВЕСЬ банковый контур (банкнота+терминолог+классификатор) в shipping-c1 НЕ включён** — жив ран-локальным конфигом книги (строка 140; сверка STACK.md 09.08 — факт шире прежней декларации «одна банкнота»); эскалация в shipping за `budget_usd: 0` (STACK.md §примечания). -> - **ЕДИНЫЙ БЭКЛОГ — секция «Бэклог» ниже** (одна таблица, единственный трекер; каждая петля обязана иметь диспозицию: решено / отложено-с-записью / отклонено; ведёт оркестратор). **СЧЁТ ОЧЕРЕДИ на 28.08 (скриптом по таблице — `python3 docs/scripts/counts.py`; обновлять при каждом лендинге):** всего **178** строк · зона бэкенд **86** строго / **122** широко (175 пере-скоуплена D39.134; 176/177 заведены 15.08; 178 — D39.136; 179 ЗАКРЫТА D39.138; 180/181 — D39.137; 182 — wire-батч, аудит 15.08; 183–187 — контракт-ревью 28, D39.138; 188–190 — аудит бэклога, D39.140; 191/192 — модель подписи банка и пост-ридинговый цикл, D39.144; 193 — молчащие дыры выдачи, D39.147; 194–196 — приёмка пака честности, D39.149; 197 — фикс-лист ФЧ, D39.150; **198–204 — приёмка P7 и слова владельца 20.08, D39.153; 205 — слепота гейта якорей, аудит 21.08; 206–207 — приёмка P8-FIX, D39.154; 208–209 — аудит доков 22.08 (потеря глоссария на Gemini, риг живых проб); 210 — трассировка цепи банка 22.08 (род не производится авто-путём); **211–213 — консилиум шва 22–23.08: ключи провайдеров не доезжают до движка на SaaS (блокер 202), нестрогий загрузчик сида, дубль конвенции пути у платформы — 211 ЗАКРЫТА и 213 сужена лендингом P9 (D39.162); 214–216 — разбор журнала трассировки банка: подпись не оставляет следа, $0-репин недостижим с платформы, три несущих сценария не проверены живым движком; 217–218, 220 — аудит переезда машины 24.08: абсолютные пути полигона, дефолты корпуса в движке, бутстрап гейта доков чужой замороженной зоной; 219 (съехавшие line-якоря D-лога) ЗАКРЫТА тем же заходом — D39.157 п.6**) (⚠ колонки счётчик читает С КОНЦА — испр. D39.116) · **блокеров очереди 0**, платные прогоны разблокированы · «скоро» **48** (перечень — грепом по таблице, рукописный список снят D39.126) · гейт-строки эксп-22: 55·153 (плюс 150 — руки владельца); строка 5 — остаток гейчен ре-пробой 74, носитель события теперь строка 188 (аудит D39.140); остальное «когда-нибудь». ⚠ Счёт — НИЖНЯЯ граница долга, не потолок (разбор — легенда таблицы ниже). +> - **ЕДИНЫЙ БЭКЛОГ — секция «Бэклог» ниже** (одна таблица, единственный трекер; каждая петля обязана иметь диспозицию: решено / отложено-с-записью / отклонено; ведёт оркестратор). **СЧЁТ ОЧЕРЕДИ на 28.08 (скриптом по таблице — `python3 docs/scripts/counts.py`; обновлять при каждом лендинге):** всего **177** строк · зона бэкенд **85** строго / **121** широко (175 пере-скоуплена D39.134; 176/177 заведены 15.08; 178 — D39.136; 179 ЗАКРЫТА D39.138; 180/181 — D39.137; 182 — wire-батч, аудит 15.08; 183–187 — контракт-ревью 28, D39.138; 188–190 — аудит бэклога, D39.140; 191/192 — модель подписи банка и пост-ридинговый цикл, D39.144; 193 — молчащие дыры выдачи, D39.147; 194–196 — приёмка пака честности, D39.149; 197 — фикс-лист ФЧ, D39.150; **198–204 — приёмка P7 и слова владельца 20.08, D39.153; 205 — слепота гейта якорей, аудит 21.08; 206–207 — приёмка P8-FIX, D39.154; 208–209 — аудит доков 22.08 (потеря глоссария на Gemini, риг живых проб); 210 — трассировка цепи банка 22.08 (род не производится авто-путём); **211–213 — консилиум шва 22–23.08: ключи провайдеров не доезжают до движка на SaaS (блокер 202), нестрогий загрузчик сида, дубль конвенции пути у платформы — 211 ЗАКРЫТА и 213 сужена лендингом P9 (D39.162); 214–216 — разбор журнала трассировки банка: подпись не оставляет следа, $0-репин недостижим с платформы, три несущих сценария не проверены живым движком; 217–218, 220 — аудит переезда машины 24.08: абсолютные пути полигона, дефолты корпуса в движке, бутстрап гейта доков чужой замороженной зоной; 219 (съехавшие line-якоря D-лога) ЗАКРЫТА тем же заходом — D39.157 п.6**) (⚠ колонки счётчик читает С КОНЦА — испр. D39.116) · **блокеров очереди 0**, платные прогоны разблокированы · «скоро» **48** (перечень — грепом по таблице, рукописный список снят D39.126) · гейт-строки эксп-22: 55·153 (плюс 150 — руки владельца); строка 5 — остаток гейчен ре-пробой 74, носитель события теперь строка 188 (аудит D39.140); остальное «когда-нибудь». ⚠ Счёт — НИЖНЯЯ граница долга, не потолок (разбор — легенда таблицы ниже). > - Архивы хроники: `archive/PROGRESS-2026-07-04-10.md` (D31) · `-10-13` (D39.6-гигиена) · `-13-25` (стройка паков 11–16, rerun2) · **`-25-31` (паки 17–20 · мини-прогон · полигон-пакеты 5–8 · ToS · холодный прогон; D39.26–58)** · **`-08-01-02` (сессии №9/№10: общность · качество банка · coldrun-b · открытие фронта/платформы; D39.59–90, срез D39.105)** · **`-08-02-04` (сессии №11–№13: ручки эффорта · стандарты · контракт API · платформа P0 · банк-арбитраж; D39.91–105 + снимок шапки эры №15)** · `-08-04-09` (пинги закрытых паков эры №15) · **`-08-14-15` (закрытые бэкенд-записи №16–17: эмиттер шва · migrate · пере-пин DeepSeek; вынесено D39.139)**. Записи ниже — живой хвост (№16+, эра D39.124+; подрезка D39.139). ## 📌 СОСТОЯНИЕ ПАКОВ (оркестратор №19, 29.08) @@ -194,7 +194,6 @@ | 232 | **Ось «свежий/пере-делка» выведена из ПОЛНОТЫ СТРОК, а не из факта отгрузки** (D39.170, находки охотника 3 и 4). Следствия ДЕНЕЖНЫЕ на слух покупателя: добавление стадии в пайплайн превращает ДОЧИТАННУЮ книгу в «3 unit(s) NEVER delivered» и приглашает купить её снова; юнит, прерванный между волнами (signature stop, денежный потолок, Ctrl-C), второй раз считается свежим и тратит слот гранта повторно — замерено 4 купленных юнита → 2 главы. Носитель у движка УЖЕ есть: реестр анонсов `events_outbox.once_key` (`backend/internal/pipeline/events.go:396`=`unitOnceKey is the identity of one announcement`), ключ `unit:<книга>:<волна>:<глава>:<юнит>`, монотонный на всю жизнь книги и переживающий и добавление стадии, и обрыв между волнами. Не хватает ЧИТАЮЩЕГО метода поверх готовой константы (`backend/internal/store/outbox.go:31`=`SELECT 1 FROM events_outbox WHERE once_key = ? AND once_key <> ''`, с уже написанным объяснением, почему хвост `<> ''` синтаксически обязателен). ⚠ РАЗВИЛКА, которую надо назвать в промте: ключ несёт ВОЛНУ, значит «юнит отгружен» — факт per-wave. **Ответ есть и он не новый механизм:** отгрузкой считается волна, владеющая ОТГРУЖАЮЩЕЙ (последней) стадией — `backend/internal/pipeline/snapshot.go:243`=`finalStageWave is the wave that owns the SHIPPING (last) stage`; на редакторском конвейере это edit, на черновом-только — draft. Проверять надо ИМ, иначе черновой-только конвейер получит ось, которая никогда ничего не считает отгруженным (предложено движковой сессией при сдаче, сверено мной по коду) | бэкенд | K | Читающий метод стора + перевод оси на факт отгрузки; отдельный пак | приёмка D39.170 | | 233 | **Трата терминолога вне объёмного потолка масштабируется КНИГОЙ, а не грантом** (D39.170). Замер: три последовательные покупки по одному юниту на четырёхглавной книге дали три полнокнижных консолидации по $0.005460 каждая — покупка одного юнита обходится в ~1.5× стоимости самого юнита. Книга на 500 юнитов, проданная по одному, оплатит 500 полнокнижных проходов. Место траты — в ЦЕНЕ, а не в потолке (решение подтверждено), но при мелкой нарезке продажи она перестаёт быть накладной и становится основной статьёй: это ВХОД В КАЛИБРОВКУ ЦЕНЫ, а не сноска | бэкенд | K | Учесть в модели цены при следующей калибровке; либо чекпойнт консолидации, переживающий покупку | приёмка D39.170 | | 234 | **Основание платформы «не брать `rebill_*` через шов» УСТАРЕЛО этим же лендингом** (D39.170). `platform/internal/ingest/resync.go:37-43` объясняет отказ ТАЙМИНГОМ: «status проецирует СОХРАНЁННУЮ память, и сразу после `bank-apply` он честно читает ноль». Движковый пак это починил: `foldMemoryForRead` стал ПЕРВЫМ ответом читающего пути, `projectStoredMemory` понижена до фолбэка (`backend/internal/pipeline/status.go:817-824`=`IT IS NO LONGER THE READ PATH'S FIRST ANSWER`). Комментарий чужой зоны теперь несёт снятую посылку и будет прочитан следующей сессией как действующий довод. ⚠ Проводка полей при этом НЕ разблокирована: она гейчена вместе с `--max-units` | платформа | K | Пинг зоне платформы + строка её регистра; проводка — после снятия гейта `--max-units` | приёмка D39.170 | -| 235 | **У движка нет гейта «каждый SQL-оператор планируется против мигрированной схемы»** — класса, который платформа закрыла своим `pgstore.TestEverySQLStatementParsesAgainstTheMigratedSchema` и который там ловит «нет такой колонки» для ВСЕХ 172 операторов, включая склеенные. У движка **59 вызовов** (`backend/internal/store/`, SQLite через `modernc.org/sqlite`, весь SQL рукописный, генерации нет), и поиск по `*_test.go` не находит ни обхода всех операторов, ни теста на дрейф схемы: единственное родственное — `outbox_test.go:269`=`EXPLAIN QUERY PLAN` — про использование индекса, а не про схему. Значит правка миграции, разошедшаяся с запросом, ловится только тем тестом, который случайно этот путь исполняет. ⚠ **Приоритет над расширением `sqlc`:** разбор D39.155 п.9 и приёмка D39.172 сходятся в том, что зоне больше дал ГЕЙТ, а не генератор; для 59 запросов это один небольшой пак. Форма — переносить механику платформы (сбор операторов экстрактором + план каждого против схемы, поднятой миграциями), а не изобретать: у неё есть пол на сам экстрактор, чтобы «замолчавший сборщик» не превратил гейт в зелёный свет над пустотой. | бэкенд | K | Отдельный пак: экстрактор + прогон каждого оператора против мигрированной SQLite-схемы + пол на экстрактор | вопрос владельца 30.08, числа сверены мной | | 217 | **Скрипты полигона захардкодили АБСОЛЮТНЫЕ пути двух корней и не работают на этой машине вовсе.** ДВА РАЗНЫХ слома. **Корень репо** — смена пользователя: литерал в 121 файле, из них 104 дословно `REPO = Path("/home/ubuntu/projects/textmachine")`, портабельных форм всего 5. **Корень книг** — оба события порознь: литерал `/home/ubuntu/books` 86 файлов (пользователь), портабельный `Path.home()/"books"` 89 файлов (переезд каталога; на прежних машинах работал). Объединение 173 файла. Пользователь теперь `ubuntu-26`, книги переехали в `<репозиторий>/books` ⇒ ломается и то и другое, включая ЖИВОЙ гейт фазы Д `eval/conformance.py:48`=`REPO = Path("/home/ubuntu/projects/textmachine")`. Живых носителей 26 по корню книг (среди них портабельная форма преобладает 20:6) плюс корневые гейты и сборщики корпусов; остальное — скрипты закрытых эксп-12–16, их владелец велел ВЫБРОСИТЬ, а не чинить (D39.157 п.4а). Форма правки — за зоной; приор оркестратора: один модуль путей, корень резолвится МАРКЕРОМ вверх по дереву (движок так и делает — `backend/internal/miner/miner_parity_test.go:29`=`derived from the repository MARKER`), книги от него, обе ручки перекрываются env | полигон | скоро (блокирует любой прогон зоны) | пак путей полигона | переезд машины 24.08, №19 | | 219 | **Line-якоря в тела D-лога обречены съезжать, и механизм именно в дисциплине эррат.** Эрраты вписываются в КАРТУ ШАПКИ (append-only, D23.3), то есть в начало файла, — значит каждая эррата сдвигает номера строк ВСЕХ тел ниже, и любой якорь вида `05-decisions-log.md:NNN` умирает молча. ⚠ Замерено на себе 27.08: три эрраты за сессию убили якоря в `17-seam-inbound-law.md:120` и `research/25-seam-cold-review.md:5` (оба целили в `:232`, тело уехало на `:239`). Лечение — не пере-нацеливание (оно повторится через эррату), а СМЕНА ФОРМЫ якоря: в тела D-лога целиться номером ноты (`^## D39.106`), который стабилен навсегда, а не строкой. ⚠ Строка ОБЪЯВЛЕНА в CURRENT-STATE 24.08 и до 27.08 в таблице НЕ СУЩЕСТВОВАЛА — очередь три дня ссылалась на носитель, которого нет; поймано свипом якорей при выдаче контрактного минора | оркестратор | скоро (растёт с каждой эрратой) | `--lint` учит форму `файл:^## D<номер>`, живые якоря в D-лог переводятся на неё | ревью доков 24.08, механизм и отсутствие носителя — 27.08, D39.160 | | 220 | **Репо-широкий гейт доков бутстрапится пакетным менеджером ОДНОЙ зоны, и та заморожена.** Диспетчер `.git/hooks/pre-commit` зоно-нейтрален по построению (`for hook in */scripts/githooks/pre-commit`, `#!/bin/sh`, node не нужен) и судит `CLAUDE.md` + `docs/**` + `platform/docs/**` + `frontend/docs/**` (`docs/scripts/counts.py:500`=`ROOT / "CLAUDE.md"`), но единственный его УСТАНОВЩИК — `frontend/scripts/githooks/install.mjs`, вызываемый ключом `prepare` из `frontend/package.json`. Охват и бутстрап не совпадают ⇒ инструмент решает свою задачу не в полной мере. Проверено исполнением 24.08: на этой машине `.git/hooks/` содержал только `*.sample`, гейт МОЛЧАЛ, ничего не проверив, и поймал первые четыре дефекта только после ручной установки; сессия бэкенда или доков `npm install` не делает никогда. Приор: зоно-нейтральный установщик в `docs/scripts/githooks/` как ЕДИНСТВЕННЫЙ писатель диспетчера, `prepare` фронта зовёт его — фронтовая половина выписана пингом №19 в зонный журнал фронта. ⚠ Ручная установка — временная мера, пока строка открыта; записывать её как порядок работы ЗАПРЕЩЕНО (это ровно тот обход, цену которого называет шапка `CLAUDE.md`) | доки + фронт | скоро | зоно-нейтральный бутстрап + правка `prepare` при разморозке | аудит переезда 24.08, №19 | diff --git a/docs/architecture/05-decisions-index.md b/docs/architecture/05-decisions-index.md index a9a34dd0..f0be122d 100644 --- a/docs/architecture/05-decisions-index.md +++ b/docs/architecture/05-decisions-index.md @@ -1,4 +1,4 @@ -# Реестр D-нот — карта актуальности v2 (D1–D39.172; +# Реестр D-нот — карта актуальности v2 (D1–D39.173; > ⚠ **СЛАБОЕ МЕСТО, КОТОРОЕ БЫЛО ЗДЕСЬ (вписано 22.08, ЗАКРЫТО 24.08 — D39.157 п.6).** Колонка ТЕЛА > у нот D39.107…D39.123 говорила «жив», хотя тела уехали в слайс подрезкой D39.139; семнадцать строк @@ -233,3 +233,4 @@ | 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 чужих операций под усечением). Оба починены структурно (рукопожатие + сверка прироста; усечение только на предмет), боевой код не тронут. **Норма: тест не имеет права утверждать о ВРЕМЕНИ, если предмет утверждения — не время; тихо-зелёное дороже красного.** | жив | ЖИВОЕ: две замеренные формы дефектного теста; узость класса проверяется по формам и зоне, а не вообще | тесты гейты движок приёмка | | D39.172 | 29.08 | **Пак `sqlc` принят и заленджен**: 40 запросов пяти файлов `pgstore` на типизированный слой, пин 1.31.1, `sqlc diff` в `make check` + гейт без установленного инструмента. ⚠ Решение принято НЕ тем доводом, которым заказывалось: счёт покрытия дал «почти ничего» (3 из 42), а шесть посаженных мутаций ВЫЖИЛИ при зелёной батарее — `sqlgate` видит строку SQL и никогда Go-сторону вызова. Подтверждено независимо на денежном пути: перестановка целей `Scan` в `ReadAccount` зелена на всей батарее и переворачивает числа оператора при дрейфе (`PD-430`). Честные границы: `observe.go` структурно не конвертируется (River мигрирует `river_job` сам), худший позиционный дрейф остался рукописным; `Touch` выбрасывает `RowsAffected` — не чинится конверсией. | жив | ЖИВОЕ: П-19 править на 40 конвертируемых; `Touch` строкой; пересборка `tmctl` в рецепте стенда; `PD-423` пере-проверить | платформа sqlc гейты деньги | +| D39.173 | 30.08 | **У движка появился гейт схемы**: каждый SQL-оператор компилируется `db.Prepare` против схемы, поднятой ПРОДОВОЙ цепью миграций во временный файл — раньше покрытие этого класса равнялось покрытию батареи. ОДИН новый тестовый файл, продовый код не тронут. Склейки не пропускаются, а раскладываются (перебор четырёх таблиц, условная сборка из двух ветвей). 70 операторов против схемы v16 за 0.02 с, пол экстрактора 65. Расхождений в живом коде нет. Внешнего не требуется — у SQLite схема это файл. | жив | ЖИВОЕ: гейт не доказывает верность оператора, только существование имён; пол 65 — суждение | движок гейты схема тесты | diff --git a/docs/architecture/05-decisions-log.md b/docs/architecture/05-decisions-log.md index 2e056a3a..122c2840 100644 --- a/docs/architecture/05-decisions-log.md +++ b/docs/architecture/05-decisions-log.md @@ -1,4 +1,4 @@ -# Журнал решений оркестратора — контракт D1–D39.172 (живой файл: карта · эрраты · живые тела · голова D39.124+ (подрезка D39.139); тела закрытых эр — в слайсах `docs/archive/architecture/`, указатель ниже; реестр всех нот — `05-decisions-index.md`) +# Журнал решений оркестратора — контракт D1–D39.173 (живой файл: карта · эрраты · живые тела · голова 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+). @@ -1470,3 +1470,47 @@ head-1 это семнадцать бюджетируемых операций, Рецепт в `STACK_DECISIONS` про пересборку не говорит; строка заведена. Отдельно: **`PD-423` у сессии НЕ воспроизвёлся** — `TestARunIsBoundedByItsOwnCgroup` зелёный в трёх прогонах при cgroup `/init.scope`, то есть условие перемежающееся, и строку надо пере-проверить, а не закрывать. + +## D39.173 — У ДВИЖКА ПОЯВИЛСЯ ГЕЙТ СХЕМЫ: каждый SQL-оператор компилируется против мигрированной схемы, а не против удачи покрытия (30.08, оркестратор №19). ✅ + +**Что заленджено.** `backend/internal/store/sqlgate_test.go` — ОДИН новый файл, только тест. **Продовый +код не тронут ни строкой, ни один существующий тест не изменён.** Гейт закрывает класс «миграция и +запрос разошлись»: раньше его покрытие равнялось покрытию батареи, и разницу никто не отслеживал. + +**Что построено и чем это отличается от платформенного оригинала.** Проверка — `db.Prepare`, а не +`EXPLAIN` и не исполнение: подготовка резолвит каждое имя таблицы, колонки и функции, оставляя +параметры несвязанными; исполнение заставило бы гейт выдумывать значения и писать в базу, которую он +осматривает. Схема поднимается прогоном ПРОДОВОЙ цепи миграций во временный файл, поэтому цепь заодно +становится самопроверяемой — сломанная миграция валит тест раньше, чем прочитан первый запрос, — и +сверяется с `SchemaHead()`: «Open вернулся» не то же самое, что «цепь дошла до головы». **Внешнего не +требуется ничего:** у SQLite схема это файл, гейт держится на чистой машине без стенда и переменных — +в отличие от платформенного, которому нужен живой Postgres. + +⚠ **Склейки не пропускаются, а РАСКЛАДЫВАЮТСЯ, и это сильнее оригинала.** Обе склейки движка дают +больше одного оператора: перебор четырёх имён таблиц (`glossary.go:145`) и условная сборка из двух +веток (`ledger.go:334`), где ветка с фильтром достижима лишь иногда — ровно то, ради чего гейт и нужен. +Свернувший только первый вариант объявил бы остальные проверенными. Что не раскладывается — ОШИБКА, а +не пропуск. + +**Приёмка — исполнением, мои посадки поверх её.** Батарея целиком: `EXIT=0`, 17 пакетов, линтер +`0 issues`; сам гейт — **70 операторов против схемы v16 за 0.02 с**. Моя посадка «колонка переименована +в ЗАПРОСЕ» поймана адресно, с файлом, строкой, колонкой и текстом оператора. Моя посадка «экстрактор +ослеплён на один вид вызова» поймана полом: `resolved 38 statements`, порог 65. + +**Расхождений в живом коде гейт НЕ нашёл** — все 70 операторов компилируются. То есть дыра закрыта до +того, как выстрелила, а не после: в платформе тот же класс дважды давал падения в рантайме. + +**Честные границы, названные сессией против себя.** (а) Гейт не доказывает, что оператор ВЕРНЫЙ — +только что каждое имя существует и SQLite его компилирует; логика, джойны, типы под параметрами и скан +в поля Go вне его, и это написано в доккомментарии. (б) Пол 65 — суждение, а не факт: законный +рефактор, убравший шесть операторов, даст ложное красное. (в) **Вклад гейта во время батареи +неизмерим** — его собственная стоимость 0.02 с, а разброс между прогонами около 30 с, на три порядка +больше; сессия отказалась записывать себе ускорение, которого не было. + +⚠ **Два урока процесса, оба названы сессией сами.** Первая посадка ушла мимо цели: переименование +таблицы в её `CREATE` уронило сборку схемы, а не запрос — у гейта два режима отказа, и в логе их легко +перепутать. И второй раз за двое суток сессия начала с `go test` вместо `make battery`, и линтер поймал +её на собственной строке; починила по существу, а не подавлением. Тот же урок, на котором я попался +позавчера. + +**Строка 235 единого бэклога ЗАКРЫТА.**