diff --git a/backend/cmd/tmctl/invocation_test.go b/backend/cmd/tmctl/invocation_test.go index c2b9eec2..eb1f8b81 100644 --- a/backend/cmd/tmctl/invocation_test.go +++ b/backend/cmd/tmctl/invocation_test.go @@ -107,4 +107,17 @@ func TestExitCodeContract(t *testing.T) { if exitCode(fmt.Errorf("outer: %w", sentinel)) != 2 { t.Fatal("wrapped sentinel → 2") } + // R1-FL-A: a bank-mining signature stop is a DISTINCT exit code (3), not conflated with a flag (2) or a + // crash (1), so exit-code automation can tell a sign-boundary pause from a real failure. + sigStop := &pipeline.WaveSignatureStop{Terms: 4, SignaturePath: "/tmp/book.db.mined-signature.yaml"} + if exitCode(sigStop) != 3 { + t.Fatalf("signature-stop sentinel → 3, got %d", exitCode(sigStop)) + } + if exitCode(fmt.Errorf("outer: %w", sigStop)) != 3 { + t.Fatal("wrapped signature-stop → 3") + } + // The two sentinels must NOT collide: a flag stays 2 even though both are typed sentinels. + if exitCode(sentinel) == exitCode(sigStop) { + t.Fatal("CompletedWithFlags and WaveSignatureStop must map to different exit codes") + } } diff --git a/backend/cmd/tmctl/main.go b/backend/cmd/tmctl/main.go index 71bab930..efe4d552 100644 --- a/backend/cmd/tmctl/main.go +++ b/backend/cmd/tmctl/main.go @@ -26,16 +26,23 @@ func main() { os.Exit(exitCode(err)) } -// exitCode maps a run() error onto the ratified shell contract (Веха 2): 0 clean · -// 2 completed-with-flags (приёмка допускает N флагов; типизированный сентинел -// *pipeline.CompletedWithFlags, переживает %w-обёртки через errors.As) · 1 infra -// failure и всё остальное, включая ошибку разбора флагов (ContinueOnError в -// parseInvocation держит 2 эксклюзивным для флагнутых чанков). +// exitCode maps a run() error onto the ratified shell contract (Веха 2 / R1-FL-A): 0 clean · 2 +// completed-with-flags (приёмка допускает N флагов; типизированный сентинел +// *pipeline.CompletedWithFlags, переживает %w-обёртки через errors.As) · 3 bank-mining +// signature stop (*pipeline.WaveSignatureStop — the run paused before the edit wave for owner +// sign; a DELIBERATE human-in-the-loop halt, not a crash) · 1 infra failure и всё остальное, +// включая ошибку разбора флагов. 3 is DISTINCT from both 2 (flagged chunks) and 1 (crash) so +// exit-code automation can tell a sign-boundary pause from a real failure — a human sees the +// stderr text either way, but a script that shipped "translate && export" must NOT treat the +// pause as success (exit 0) nor as an infra crash (exit 1). func exitCode(err error) int { var flagged *pipeline.CompletedWithFlags + var sigStop *pipeline.WaveSignatureStop switch { case err == nil: return 0 + case errors.As(err, &sigStop): + return 3 case errors.As(err, &flagged): return 2 default: @@ -89,6 +96,13 @@ func translate(ctx context.Context, cfgPath string, resnapshot bool) error { res, err := r.TranslateBook(ctx) if err != nil { + // R1-FL-A: the bank-mining signature stop is a typed sentinel, not an infra failure — render the + // operator's next steps to stdout (terms + signature-map path + resume guidance) and return it so + // main() maps it to the distinct exit code 3 (the Error() text also prints to stderr). + var sigStop *pipeline.WaveSignatureStop + if errors.As(err, &sigStop) { + renderSignatureStop(os.Stdout, sigStop) + } return err } return renderTranslate(os.Stdout, res, func() (float64, float64, error) { diff --git a/backend/cmd/tmctl/render.go b/backend/cmd/tmctl/render.go index aa648ff6..0455eec4 100644 --- a/backend/cmd/tmctl/render.go +++ b/backend/cmd/tmctl/render.go @@ -70,6 +70,19 @@ func renderTranslate(w io.Writer, res *pipeline.BookResult, ledger func() (commi return nil } +// renderSignatureStop prints the operator-facing bank-mining signature-stop banner (R1-FL-A): the run +// paused before the edit wave because the miner proposed N new terms for owner sign. It surfaces the term +// count + the signature-map path + the resume path (promote a term into the mined-delta file OR decline it +// in the mined-rejects file, then re-run). Distinct from a flag/crash — main() maps the sentinel to exit 3. +func renderSignatureStop(w io.Writer, s *pipeline.WaveSignatureStop) { + fmt.Fprintf(w, "=== БАНК-МАЙНИНГ: СТОП НА ПОДПИСЬ (%d новых терм(ов)) ===\n", s.Terms) + fmt.Fprintf(w, "Прогон остановлен ПЕРЕД волной редактуры: майнер предложил %d новых терм(ов) на подпись владельца.\n", s.Terms) + fmt.Fprintf(w, "Карта подписи: %s\n", s.SignaturePath) + fmt.Fprintln(w, "Дальше: по каждому терму ЛИБО промоутни его в mined-delta файл (approved + dst), ЛИБО отклони в") + fmt.Fprintln(w, "mined-rejects файле (book.yaml: mined_rejects), затем перезапусти `tmctl translate`. Стоп очистится,") + fmt.Fprintln(w, "когда каждый предложенный терм промоутнут или отклонён (тогда дельта пуста → авто-продолжение к редактуре).") +} + // flagSuffix renders a non-empty flag reason as "(reason)". func flagSuffix(r pipeline.FlagReason) string { if r == "" { @@ -217,14 +230,16 @@ func errTail(degraded, errText string) string { // function's frozen callback contract (render_test.go) is untouched. func renderQuality(w io.Writer, q *pipeline.QualityReport) error { fmt.Fprintf(w, "\n=== КАЧЕСТВО (per-run, детерминированные сигналы — наблюдаемость, не гейт) ===\n") - fmt.Fprintf(w, "чанков всего=%d · дошло до финала=%d · с экспорт-текстом=%d\n", q.TotalChunks, q.ProcessedChunks, q.TextChunks) + fmt.Fprintf(w, "единиц всего=%d · дошло до финала=%d · с экспорт-текстом=%d\n", q.TotalUnits, q.ProcessedUnits, q.TextUnits) // Claim-1: рубленые абзацы. ≈1.0 предл./абзац = рублено (претензия владельца); выше = слитая проза. fmt.Fprintf(w, "СТРУКТУРА (претензия-1 «рубленые абзацы»): предл/нарратив-абзац=%.2f (предложений=%d / нарратив-абзацев=%d)\n", q.MeanSentPerNarrPara, q.NarrativeSentences, q.NarrativeParagraphs) fmt.Fprintf(w, "СИГНАЛЫ: диалог-тире=%d · глоссарий-промахов=%d · число-дрейф=%d · trust-gated(сид)=%d\n", q.DialogueDashFlags, q.GlossaryMisses, q.NumberDriftFlags, q.TrustGated) - fmt.Fprintf(w, "СТРИПЫ/УТЕЧКИ: косметик-strip чанков=%d (%.1f%%, markdown+CJK) · echo(cjk_artifact) чанков=%d (%.1f%%)\n", - q.CosmeticStripChunks, 100*q.CosmeticStripRate, q.EchoChunks, 100*q.EchoRate) + // Echo split (D39.18): draft = переводчик эхнул (вкл. дропнутых c-lite членов) / edit = редактор эхнул в + // доставленном. Косметик-strip — по единицам (санитайзер бежит на финальной стадии). + fmt.Fprintf(w, "СТРИПЫ/ЭХО: косметик-strip единиц=%d (%.1f%%, markdown+CJK) · echo черновик=%d (%.1f%%) · echo редактура=%d (%.1f%%)\n", + q.CosmeticStripUnits, 100*q.CosmeticStripRate, q.EchoDraftChunks, 100*q.EchoDraftRate, q.EchoEditUnits, 100*q.EchoEditRate) return nil } @@ -238,8 +253,8 @@ func renderExport(w io.Writer, exp *pipeline.BookExport, asPlaintext bool) error if asPlaintext { // Manifest/drift summary first (F3/F4): a partial or drifted book is EXPLICIT, not silently // exported as complete. - fmt.Fprintf(w, "# export %s — чанков всего=%d, экспортировано=%d, ожидают=%d", - exp.BookID, exp.TotalChunks, exp.TotalChunks-exp.PendingChunks, exp.PendingChunks) + fmt.Fprintf(w, "# export %s — единиц всего=%d, экспортировано=%d, ожидают=%d", + exp.BookID, exp.TotalUnits, exp.TotalUnits-exp.PendingUnits, exp.PendingUnits) if exp.GhostRows > 0 { fmt.Fprintf(w, ", ghost-строк-отброшено=%d", exp.GhostRows) } @@ -292,7 +307,7 @@ func renderStatusJSON(w io.Writer, rep *pipeline.StatusReport) error { // (its message goes to stderr in main), so a CI/IDE consumer both parses the projection AND // sees "attention needed" in the exit code instead of silently reading 0. if rep.Flagged > 0 { - return &pipeline.CompletedWithFlags{Flagged: rep.Flagged, Total: rep.TotalChunks} + return &pipeline.CompletedWithFlags{Flagged: rep.Flagged, Total: rep.TotalUnits} } return nil } @@ -315,8 +330,8 @@ func renderStatusHuman(w io.Writer, rep *pipeline.StatusReport, cfgPath string) drift += " ⚠ CONFIG-DRIFT (текущий конфиг рендерит другой снапшот — правка после прогона; translate потребует --resnapshot = переоплата книги)" } fmt.Fprintf(w, "=== СТАТУС: %s (snapshot %s)%s ===\n", rep.BookID, snap, drift) - fmt.Fprintf(w, "Прогресс: %d/%d чанков (%.1f%%) — done=%d in_progress=%d flagged=%d pending=%d\n", - rep.Done, rep.TotalChunks, rep.PercentDone, rep.Done, rep.InProgress, rep.Flagged, rep.Pending) + fmt.Fprintf(w, "Прогресс: %d/%d единиц (%.1f%%) — done=%d in_progress=%d flagged=%d pending=%d\n", + rep.Done, rep.TotalUnits, rep.PercentDone, rep.Done, rep.InProgress, rep.Flagged, rep.Pending) fmt.Fprintf(w, "Эскалаций: %d · post-check промахов (confirmed): %d · стиль-флагов (наблюдаемость): %d\n", rep.Escalations, rep.PostcheckMisses, rep.StyleFlags) ceil := "" @@ -335,7 +350,7 @@ func renderStatusHuman(w io.Writer, rep *pipeline.StatusReport, cfgPath string) fmt.Fprintf(w, "\n%-5s %-9s %-10s %-6s %-4s %-6s %-16s %10s\n", "ch", "done/all", "verdict", "flag", "esc", "style", "worst_flag", "cost_usd") for _, p := range rep.Chapters { fmt.Fprintf(w, "%-5d %d/%-7d %-10s %-6d %-4d %-6d %-16s %10.6f\n", - p.Chapter, p.ChunksDone, p.ChunksTotal, p.Verdict, p.ChunksFlagged, p.Escalations, p.StyleFlags, + p.Chapter, p.UnitsDone, p.UnitsTotal, p.Verdict, p.UnitsFlagged, p.Escalations, p.StyleFlags, dashIfEmpty(p.WorstFlagReason), p.CostUSD) } } @@ -353,7 +368,7 @@ func renderStatusHuman(w io.Writer, rep *pipeline.StatusReport, cfgPath string) fmt.Fprintf(w, "\n%d чанк(ов) флагнуты post-check-гейтом (glossary_miss) — redrive для них no-op: исправьте сид-глоссарий (термин/dst) и перезапустите `tmctl translate --config %s --resnapshot` (переоплата затронутых чанков)\n", rep.GlossaryMissFlagged, cfgPath) } - return &pipeline.CompletedWithFlags{Flagged: rep.Flagged, Total: rep.TotalChunks} + return &pipeline.CompletedWithFlags{Flagged: rep.Flagged, Total: rep.TotalUnits} } return nil } diff --git a/backend/cmd/tmctl/render_test.go b/backend/cmd/tmctl/render_test.go index 3ab8bcca..f51ab6d5 100644 --- a/backend/cmd/tmctl/render_test.go +++ b/backend/cmd/tmctl/render_test.go @@ -174,7 +174,7 @@ func TestRenderReportPartialOutputOnMidAuditError(t *testing.T) { func TestRenderStatusJSONSchemaAndSentinel(t *testing.T) { var b strings.Builder - rep := &pipeline.StatusReport{BookID: "b1", TotalChunks: 4, Done: 3, Flagged: 1} + rep := &pipeline.StatusReport{BookID: "b1", TotalUnits: 4, Done: 3, Flagged: 1} err := renderStatusJSON(&b, rep) var flagged *pipeline.CompletedWithFlags if !errors.As(err, &flagged) { @@ -188,7 +188,7 @@ func TestRenderStatusJSONSchemaAndSentinel(t *testing.T) { t.Fatalf("stable schema field book_id missing: %v", decoded) } // Clean book → nil error → exit 0. - if err := renderStatusJSON(&strings.Builder{}, &pipeline.StatusReport{TotalChunks: 4, Done: 4}); err != nil { + if err := renderStatusJSON(&strings.Builder{}, &pipeline.StatusReport{TotalUnits: 4, Done: 4}); err != nil { t.Fatalf("clean book must exit 0, got %v", err) } } @@ -196,7 +196,7 @@ func TestRenderStatusJSONSchemaAndSentinel(t *testing.T) { func TestRenderStatusHumanDriftAndAdvice(t *testing.T) { var b strings.Builder rep := &pipeline.StatusReport{ - BookID: "b1", Snapshot: "abcdef0123456789", TotalChunks: 10, Done: 7, + BookID: "b1", Snapshot: "abcdef0123456789", TotalUnits: 10, Done: 7, Flagged: 3, GlossaryMissFlagged: 1, SnapshotDrift: true, ConfigDrift: true, } err := renderStatusHuman(&b, rep, "book.yaml") diff --git a/backend/internal/config/book.go b/backend/internal/config/book.go index 5446315a..e16c906c 100644 --- a/backend/internal/config/book.go +++ b/backend/internal/config/book.go @@ -69,8 +69,14 @@ type Book struct { // R1). Its terms are loaded as Source:"mined" (NOT Source:"seed"), so they fold into the ENRICHED bank // version but NOT the base — adding them moves only snapshot_W2 («переоплата ОДНА»). Same seedTerm // schema as glossary_seed; resolved relative to book.yaml. Empty = no mined terms. - MinedDelta string `yaml:"mined_delta"` - Ceilings BookCap `yaml:"ceilings"` + MinedDelta string `yaml:"mined_delta"` + // MinedRejects is the optional path to the owner's mined-term REJECT list YAML (R1-FL-B). It lists the + // src surfaces the owner reviewed and DECLINED, so the bank-mining stop stops re-proposing them every + // run (else a declined term re-fires the stop forever — a livelock). Unlike MinedDelta it is a PROPOSAL + // filter only: rejects NEVER enter the bank content, so they are deliberately NOT folded into the + // snapshot. Resolved relative to book.yaml. Empty = no rejects. + MinedRejects string `yaml:"mined_rejects"` + Ceilings BookCap `yaml:"ceilings"` } // BookCap are the ledger admission limits (Р7: потолок $ на книгу/день). @@ -103,6 +109,7 @@ func LoadBook(path string) (*Book, error) { b.GlossarySeed = resolve(b.GlossarySeed) b.LangpackRoot = resolve(b.LangpackRoot) b.MinedDelta = resolve(b.MinedDelta) + b.MinedRejects = resolve(b.MinedRejects) if b.ProjectDB == "" { b.ProjectDB = filepath.Join(dir, b.BookID+".db") } else { @@ -141,6 +148,11 @@ func LoadBook(path string) (*Book, error) { bad("mined_delta %s is not readable: %v", b.MinedDelta, err) } } + if b.MinedRejects != "" { + if _, err := os.Stat(b.MinedRejects); err != nil { + bad("mined_rejects %s is not readable: %v", b.MinedRejects, err) + } + } if b.Encoding == "" { b.Encoding = "auto" } diff --git a/backend/internal/pipeline/export.go b/backend/internal/pipeline/export.go index cf54201a..e80e719b 100644 --- a/backend/internal/pipeline/export.go +++ b/backend/internal/pipeline/export.go @@ -48,11 +48,11 @@ type ChunkExport struct { // instead of silently exporting as complete (F4/F3). type BookExport struct { BookID string `json:"book_id"` - // TotalChunks is the current source manifest size (the honest denominator); ExportedChunks + - // PendingChunks == TotalChunks. GhostRows are stored final rows dropped as OUTSIDE the manifest. - TotalChunks int `json:"total_chunks"` - PendingChunks int `json:"pending_chunks"` - GhostRows int `json:"ghost_rows,omitempty"` + // TotalUnits is the current source manifest size in output units (the honest denominator); the exported + // units + PendingUnits == TotalUnits. GhostRows are stored final rows dropped as OUTSIDE the manifest. + TotalUnits int `json:"total_units"` + PendingUnits int `json:"pending_units"` + GhostRows int `json:"ghost_rows,omitempty"` // ConfigDrift is true when the CURRENT config renders a snapshot different from the one the stored // rows carry (a gate flip / prompt bump / stage rename since the run) — the gate/stage re-derivation // below may then not match what translate actually did. CurrentSnapshot is that projected id. @@ -143,14 +143,14 @@ func (r *Runner) Export(pairs bool) (*BookExport, error) { for _, u := range units { key := chunkKey{u.Chapter, u.FirstChunkIdx} inManifest[key] = true - exp.TotalChunks++ + exp.TotalUnits++ cs, ok := finalRow[key] if !ok { pend := ChunkExport{Chapter: u.Chapter, ChunkIdx: u.FirstChunkIdx, Disposition: exportPending} if pairs { pend.Source = u.sourceText() } - exp.PendingChunks++ + exp.PendingUnits++ exp.Chunks = append(exp.Chunks, pend) continue } diff --git a/backend/internal/pipeline/export_test.go b/backend/internal/pipeline/export_test.go index 08047b3b..acc208ab 100644 --- a/backend/internal/pipeline/export_test.go +++ b/backend/internal/pipeline/export_test.go @@ -201,14 +201,14 @@ func TestExportManifestPending(t *testing.T) { if err != nil { t.Fatalf("Export: %v", err) } - if exp.TotalChunks != 3 { - t.Fatalf("manifest must be 3 chunks, got total=%d", exp.TotalChunks) + if exp.TotalUnits != 3 { + t.Fatalf("manifest must be 3 chunks, got total=%d", exp.TotalUnits) } - if exp.PendingChunks < 1 { - t.Fatalf("a ceiling-stopped book must have pending chunks, got pending=%d (%+v)", exp.PendingChunks, exp.Chunks) + if exp.PendingUnits < 1 { + t.Fatalf("a ceiling-stopped book must have pending chunks, got pending=%d (%+v)", exp.PendingUnits, exp.Chunks) } - if len(exp.Chunks) != exp.TotalChunks { - t.Fatalf("every manifest chunk must have a row: len=%d total=%d", len(exp.Chunks), exp.TotalChunks) + if len(exp.Chunks) != exp.TotalUnits { + t.Fatalf("every manifest chunk must have a row: len=%d total=%d", len(exp.Chunks), exp.TotalUnits) } var pending int for _, ce := range exp.Chunks { @@ -219,8 +219,8 @@ func TestExportManifestPending(t *testing.T) { } } } - if pending != exp.PendingChunks { - t.Errorf("pending rows (%d) must match the counter (%d)", pending, exp.PendingChunks) + if pending != exp.PendingUnits { + t.Errorf("pending rows (%d) must match the counter (%d)", pending, exp.PendingUnits) } } diff --git a/backend/internal/pipeline/memnorm.go b/backend/internal/pipeline/memnorm.go index 1d522ba4..07eefd52 100644 --- a/backend/internal/pipeline/memnorm.go +++ b/backend/internal/pipeline/memnorm.go @@ -45,9 +45,16 @@ var trad2simpRaw string // unicode.Han/Cyrillic predicates) changes this version → the snapshot moves → a LOUD --resnapshot, // never a silent match-behaviour change on already-checkpointed chunks. TRIPWIRE: do NOT bump the // toolchain past the pinned Unicode edition without a --resnapshot (we are on go1.26.4 / Unicode 15.0.0). -// (x/text/norm carries its OWN Unicode edition independent of stdlib unicode.Version — a standalone x/text -// dependency bump must be treated like a toolchain bump, i.e. a deliberate --resnapshot; residual noted.) -const memoryNormAlgoVersion = "memnorm-v3-nfkc+apos+stripignorable+trad+kana+lower/nfc+dash+apos+stripignorable+lower+yofold+u" + unicode.Version +// +// R1-FL-C: golang.org/x/text/norm carries its OWN Unicode edition INDEPENDENT of stdlib unicode.Version +// (its NFKC/NFC tables ship in the module, not the toolchain), so a standalone `go get -u golang.org/x/text` +// could silently shift normalizeSourceKey's NFKC output → a re-verdict on the post-check WITHOUT moving the +// snapshot. Weaving xTextVersion in makes an x/text bump a loud --resnapshot too. It is a PINNED constant +// (debug.ReadBuildInfo returns an empty x/text version under `go test`, where the golden is captured, so a +// build-info fold would be non-deterministic) DRIFT-CHECKED against go.mod by TestMemnormXTextVersionPin: +// a go.mod bump that forgets this constant fails that test loudly — the tripwire enforced by mechanism. +const xTextVersion = "v0.38.0" // MUST equal go.mod's golang.org/x/text (asserted by TestMemnormXTextVersionPin); a bump = --resnapshot +const memoryNormAlgoVersion = "memnorm-v3-nfkc+apos+stripignorable+trad+kana+lower/nfc+dash+apos+stripignorable+lower+yofold+u" + unicode.Version + "+xtext" + xTextVersion var ( // trad2simp is the parsed Traditional→Simplified single-char map (data/trad2simp.txt). diff --git a/backend/internal/pipeline/memnorm_test.go b/backend/internal/pipeline/memnorm_test.go index 232cbe45..fce06e07 100644 --- a/backend/internal/pipeline/memnorm_test.go +++ b/backend/internal/pipeline/memnorm_test.go @@ -1,12 +1,47 @@ package pipeline import ( + "bufio" + "os" "strings" "testing" "textmachine/backend/internal/store" ) +// TestMemnormXTextVersionPin is the R1-FL-C drift tripwire: the xTextVersion constant folded into +// memoryNormAlgoVersion MUST equal the golang.org/x/text version go.mod pins. x/text/norm ships its own +// Unicode edition, so a `go get -u golang.org/x/text` that forgets to bump the constant would silently +// shift NFKC normalization WITHOUT moving the snapshot — this test turns that into a loud failure +// (the bump is then a deliberate --resnapshot, re-capturing the golden). Reads the module-root go.mod. +func TestMemnormXTextVersionPin(t *testing.T) { + f, err := os.Open("../../go.mod") + if err != nil { + t.Fatalf("open go.mod: %v", err) + } + defer f.Close() + want := "" + sc := bufio.NewScanner(f) + for sc.Scan() { + fields := strings.Fields(sc.Text()) + // A require line: "golang.org/x/text v0.38.0" (possibly with a trailing "// indirect"). + if len(fields) >= 2 && fields[0] == "golang.org/x/text" { + want = fields[1] + break + } + } + if err := sc.Err(); err != nil { + t.Fatalf("scan go.mod: %v", err) + } + if want == "" { + t.Fatal("golang.org/x/text not found in go.mod") + } + if xTextVersion != want { + t.Fatalf("xTextVersion = %q but go.mod pins golang.org/x/text %q — bump the constant IN LOCKSTEP "+ + "(x/text carries its own Unicode edition; a bump is a deliberate --resnapshot, re-capture the golden)", xTextVersion, want) + } +} + // TestTradTableCoversKnownCases asserts the embedded trad→simp table covers every // character referenced by the eval specs / research/14 live bugs. A mutation // (deleting a mapping line) fails exactly here — the guard against silently diff --git a/backend/internal/pipeline/memseed.go b/backend/internal/pipeline/memseed.go index f08e7198..7838e14d 100644 --- a/backend/internal/pipeline/memseed.go +++ b/backend/internal/pipeline/memseed.go @@ -518,41 +518,6 @@ func SeedLint(path string) error { return nil } -// --- mined → candidates (WS3 bank-mining mined-write path) ----------------------------- - -// minedToCandidates converts the miner's WHICH proposals (MineBank) into store candidates for the the bank-mining stop -// reseed (WS3 / §1в mined-write path). It is the DEDICATED mined path the plan requires: it stamps -// Source:"mined" (mirroring rubyToCandidates' Source:"ruby"), NOT the Source:"seed" that loadGlossarySeed -// hardcodes — so a mined row is EXCLUDED from the base-bank-version (BaseVersion) and the the bank-mining stop bank -// enrichment moves ONLY snapshot_W2, keeping the draft wave checkpoints valid («переоплата ОДНА», §1в/F2). Every -// candidate is status=auto with NO dst (the WHAT is delivered by the banknote/owner sign, WS4): inert on -// the hot path until a human/batch promotes it, never a blind name-lock. A src already covered by the -// manual seed is skipped (the curated entry wins), exactly like rubyToCandidates. Deterministic (MineBank -// emits a src-sorted delta; aliases are re-sorted on read by GlossaryForBook). -func minedToCandidates(mined []MinedTerm, manualSrcs map[string]bool) []store.GlossaryEntry { - var out []store.GlossaryEntry - for _, m := range mined { - if manualSrcs[m.Src] { - continue // the curated seed already owns this src - } - var aliases []store.GlossaryAlias - seen := map[string]bool{} - for _, a := range m.Aliases { - if a == "" || a == m.Src || seen[a] { - continue - } - seen[a] = true - aliases = append(aliases, store.GlossaryAlias{Alias: a, AliasType: "mined"}) - } - out = append(out, store.GlossaryEntry{ - Src: m.Src, Dst: "", Type: m.Type, Status: "auto", Source: "mined", - SinceCh: m.SinceCh, Confidence: m.Freq, Aliases: aliases, - Note: "mined WHICH candidate (miner-v1); dst delivered by banknote/owner sign — promote before use", - }) - } - return out -} - // ruby classifier classes (deterministic HINTS for human promotion). const ( rubyClassName = "name_candidate" // all-Han base + all-kana reading: the furigana-name SHAPE diff --git a/backend/internal/pipeline/miner_emit.go b/backend/internal/pipeline/miner_emit.go index 2737daec..242364f0 100644 --- a/backend/internal/pipeline/miner_emit.go +++ b/backend/internal/pipeline/miner_emit.go @@ -43,8 +43,11 @@ type MinedTerm struct { // MineBank runs the full default-B miner over the normalized chunks and emits the alias-clustered, // filtered mined delta (WHICH candidates for owner sign). seed is the current glossary (its surfaces -// scope the non-seed filter, the fragment guard, and the alias entity/metadata). Deterministic and $0. -func MineBank(chunks []MinerChunk, contrast *Contrast, seed []store.GlossaryEntry, cfg MinerConfig, pack *lang.Pack) []MinedTerm { +// scope the non-seed filter, the fragment guard, and the alias entity/metadata). rejects is the owner's +// declined-term set (normalized src, R1-FL-B): a candidate whose entity touches a rejected surface is +// dropped from the delta so a declined term never re-fires the stop (a nil/empty rejects is a no-op — the +// emission is byte-identical to before, preserving the frozen parity). Deterministic and $0. +func MineBank(chunks []MinerChunk, contrast *Contrast, seed []store.GlossaryEntry, rejects map[string]bool, cfg MinerConfig, pack *lang.Pack) []MinedTerm { mr := mineDetect(chunks, contrast, cfg, pack) // Seed surfaces (normalized) + their metadata for the alias rules and the non-seed / fragment guards. @@ -125,7 +128,16 @@ func MineBank(chunks []MinerChunk, contrast *Contrast, seed []store.GlossaryEntr } cl := clusterOf[c.Src] // A cluster touching a seed surface is an alias-of-existing entity → the owner attaches it; skip. - if clusterHasSeed(cl, seedSurfaces) { + if clusterTouches(cl, seedSurfaces) { + markEmitted(cl, emitted) + continue + } + // A declined entity (R1-FL-B) is dropped WITH its whole cluster, so it never re-fires the stop: + // marking the cluster emitted stops a lower-ranked member from re-emitting the same entity next run + // (which would keep the delta non-empty forever). rejects[c.Src] covers a SINGLETON representative + // (cl is nil for a candidate in no multi-cluster); clusterTouches covers a rejected alias of a + // multi-member cluster. markEmitted(nil,...) is a harmless no-op for the singleton case. + if rejects[c.Src] || clusterTouches(cl, rejects) { markEmitted(cl, emitted) continue } @@ -205,9 +217,12 @@ func hasAnyType(types []string, want ...string) bool { return false } -func clusterHasSeed(cl []string, seedSurfaces map[string]bool) bool { +// clusterTouches reports whether any surface of the identity cluster is in `set` — used both for the seed +// surfaces (an alias-of-existing entity) and the reject surfaces (a declined entity), which are treated the +// same way at emission: the whole cluster is suppressed. +func clusterTouches(cl []string, set map[string]bool) bool { for _, s := range cl { - if seedSurfaces[s] { + if set[s] { return true } } diff --git a/backend/internal/pipeline/miner_test.go b/backend/internal/pipeline/miner_test.go index 7606c477..e2804150 100644 --- a/backend/internal/pipeline/miner_test.go +++ b/backend/internal/pipeline/miner_test.go @@ -285,8 +285,8 @@ func TestMineBankDeterministicAndNonSeed(t *testing.T) { chunks := mchunks(strings.Repeat("龙公来到青茅山。龙公很强。青茅山很高。", 4)) seed := []store.GlossaryEntry{{Src: "青茅山", Dst: "гора Цинмао", Type: "place", Status: "approved", Source: "seed"}} cfg := frozenMinerConfig() - a := MineBank(chunks, smallContrast(t), seed, cfg, testLangPack(t)) - b := MineBank(chunks, smallContrast(t), seed, cfg, testLangPack(t)) + a := MineBank(chunks, smallContrast(t), seed, nil, cfg, testLangPack(t)) + b := MineBank(chunks, smallContrast(t), seed, nil, cfg, testLangPack(t)) if !reflect.DeepEqual(a, b) { t.Fatalf("MineBank must be deterministic byte-for-byte:\n a=%+v\n b=%+v", a, b) } @@ -298,43 +298,62 @@ func TestMineBankDeterministicAndNonSeed(t *testing.T) { } } -func TestMinedToCandidatesStampsMinedSource(t *testing.T) { - mined := []MinedTerm{ - {Src: "龙公", Type: "name", SinceCh: 3, Freq: 7, Aliases: []string{"龙公子"}}, - {Src: "青茅山", Type: "place", SinceCh: 1, Freq: 9}, // covered by manual seed → skipped +// TestMineBankRejectSuppressesTerm pins R1-FL-B: a term the owner declined (mined_rejects) is dropped from +// the delta, so it does NOT re-fire the stop — while the OTHER proposals (which the owner has not decided) +// still fire. The stop clears only once every proposal is promoted (into the seed) or rejected. +func TestMineBankRejectSuppressesTerm(t *testing.T) { + chunks := mchunks(strings.Repeat("方源来到青茅山。方源很强。花家很大。花家的人。", 6)) + seed := []store.GlossaryEntry{{Src: "青茅山", Dst: "гора Цинмао", Type: "place", Status: "approved", Source: "seed"}} + cfg := frozenMinerConfig() + fangYuan, huaJia := normalizeSourceKey("方源"), normalizeSourceKey("花家") + + base := MineBank(chunks, smallContrast(t), seed, nil, cfg, testLangPack(t)) + if !mineHas(base, fangYuan) || !mineHas(base, huaJia) { + t.Fatalf("fixture must propose both 方源 and 花家 for the reject test to be meaningful, got %+v", base) } - manual := map[string]bool{"青茅山": true} - got := minedToCandidates(mined, manual) - if len(got) != 1 { - t.Fatalf("want 1 mined candidate (青茅山 skipped as manual), got %d", len(got)) + + // Reject 方源 (normalized as loadMinedRejects would): it must vanish, but 花家 (still undecided) fires. + got := MineBank(chunks, smallContrast(t), seed, map[string]bool{fangYuan: true}, cfg, testLangPack(t)) + if mineHas(got, fangYuan) { + t.Fatalf("a rejected term was re-emitted → the stop would livelock: %+v", got) } - e := got[0] - if e.Source != "mined" { - t.Fatalf("mined candidate Source = %q, want \"mined\" (NOT seed — else it moves base-bank-version)", e.Source) + if !mineHas(got, huaJia) { + t.Fatalf("rejecting one term must not drop an unrelated undecided term (花家): %+v", got) } - if e.Status != "auto" || e.Dst != "" { - t.Fatalf("mined candidate must be status=auto with no dst, got status=%q dst=%q", e.Status, e.Dst) + + // Reject BOTH → the delta empties → the stop auto-continues (every proposal is now decided). + both := MineBank(chunks, smallContrast(t), seed, map[string]bool{fangYuan: true, huaJia: true}, cfg, testLangPack(t)) + if len(both) != 0 { + t.Fatalf("rejecting every proposal must empty the delta (auto-continue), got %d: %+v", len(both), both) } - if e.SinceCh != 3 || e.Confidence != 7 { - t.Fatalf("mined candidate metadata wrong: %+v", e) + + // A nil / empty rejects map must be a no-op (byte-identical to base), so an unconfigured book — and the + // frozen parity — are unaffected. + if noop := MineBank(chunks, smallContrast(t), seed, map[string]bool{}, cfg, testLangPack(t)); !reflect.DeepEqual(noop, base) { + t.Fatalf("an empty rejects map must be a no-op:\n base=%+v\n got=%+v", base, noop) } - if len(e.Aliases) != 1 || e.Aliases[0].Alias != "龙公子" || e.Aliases[0].AliasType != "mined" { - t.Fatalf("mined alias wrong: %+v", e.Aliases) +} + +// mineHas reports whether the delta contains a term with the given normalized src. +func mineHas(delta []MinedTerm, src string) bool { + for _, m := range delta { + if m.Src == src { + return true + } } + return false } func TestMinedDeltaStaysBaseBankStable(t *testing.T) { // The mined delta (Source:"mined") must NOT move BaseVersion — the load-bearing «переоплата ОДНА» - // invariant (§1в): a the draft wave.5 mined-row addition moves only the enriched version. (Sibling of the - // Block-A TestBaseEnrichedMemoryVersionSplit, here through the real mined-write path.) + // invariant (§1в): a bank-mining mined-row addition moves only the enriched version. The live mined-write + // path (loadMinedDelta, mining.go) stamps Source:"mined"; this pins the invariant that stamp guarantees. + // (Sibling of the Block-A TestBaseEnrichedMemoryVersionSplit.) seed := []store.GlossaryEntry{{Src: "方源", Dst: "Фан Юань", Status: "approved", Source: "seed"}} base0 := materializeMemory(seed, false) - mined := minedToCandidates([]MinedTerm{{Src: "龙公", Type: "name", SinceCh: 1, Freq: 5}}, map[string]bool{"方源": true}) - // A mined candidate is status=auto (not folded even into enriched when gate off) — promote it to - // approved to exercise the split, as the owner sign would at the draft wave.5. - mined[0].Status = "approved" - mined[0].Dst = "Лун Гун" - enriched := append(append([]store.GlossaryEntry{}, seed...), mined...) + // An owner-promoted mined term (approved + dst, Source:"mined"), as the sign step emits it. + mined := store.GlossaryEntry{Src: "龙公", Dst: "Лун Гун", Type: "name", Status: "approved", Source: "mined", SinceCh: 1, Confidence: 5} + enriched := append(append([]store.GlossaryEntry{}, seed...), mined) bank1 := materializeMemory(enriched, false) if base0.BaseVersion() != bank1.BaseVersion() { t.Fatalf("base-bank-version moved on a Source:mined addition — draft-wave snapshot would re-bill the draft wave") diff --git a/backend/internal/pipeline/mining.go b/backend/internal/pipeline/mining.go index b910515a..55530766 100644 --- a/backend/internal/pipeline/mining.go +++ b/backend/internal/pipeline/mining.go @@ -5,6 +5,8 @@ import ( "fmt" "os" + "gopkg.in/yaml.v3" + "textmachine/backend/internal/store" ) @@ -51,7 +53,15 @@ func (r *Runner) runBankMiningStop(ctx context.Context, chunks []Chunk) (stopped if err != nil { return false, fmt.Errorf("pipeline: the bank-mining stop read glossary for mining: %w", err) } - mined := MineBank(minerChunks, contrast, seed, frozenMinerConfig(), r.pack) + // The owner's reject list excludes declined terms from the emission (R1-FL-B): a term the owner reviewed + // and rejected is dropped from the delta exactly like a seed surface, so it never re-fires the stop. The + // stop therefore clears once EVERY proposed term is EITHER promoted (into mined_delta → a seed surface) + // OR rejected (mined_rejects) — the two owner verbs that empty the delta. + rejects, err := r.loadMinedRejects() + if err != nil { + return false, err + } + mined := MineBank(minerChunks, contrast, seed, rejects, frozenMinerConfig(), r.pack) r.lastMinedCount = len(mined) if len(mined) == 0 { r.Log.InfoContext(ctx, "bank-mining: empty delta, auto-continuing to the edit wave", "book", r.Book.BookID) @@ -66,11 +76,51 @@ func (r *Runner) runBankMiningStop(ctx context.Context, chunks []Chunk) (stopped if err := os.WriteFile(r.signatureMapPath(), []byte(yamlDelta), 0o644); err != nil { return false, fmt.Errorf("pipeline: the bank-mining stop write signature map %s: %w", r.signatureMapPath(), err) } - r.Log.WarnContext(ctx, "bank-mining: new terms await owner signature; run STOPPED before the edit wave (review the signature map, promote terms into the mined-delta file, then resume)", + r.Log.WarnContext(ctx, "bank-mining: new terms await owner signature; run STOPPED before the edit wave (review the signature map, then for EACH term either promote it into the mined-delta file OR decline it in the mined-rejects file, then resume — the stop clears once every proposed term is promoted or rejected)", "book", r.Book.BookID, "terms", len(mined), "signature_map", r.signatureMapPath()) return true, nil } +// minedRejectFile is the owner's mined-term reject list (Book.MinedRejects, R1-FL-B): the src surfaces the +// owner reviewed and DECLINED. It is a PROPOSAL filter only — rejects never enter the bank content, so this +// file is deliberately NOT folded into the snapshot (a reject affects the next mining PROPOSAL, not any +// checkpoint's wire/verdict). +type minedRejectFile struct { + Rejects []minedReject `yaml:"rejects"` +} + +// minedReject is one declined mined term. Note is the owner's optional reason, ignored by the miner but +// kept so a reject list stays self-documenting (six months on, "why was this declined"). +type minedReject struct { + Src string `yaml:"src"` + Note string `yaml:"note,omitempty"` +} + +// loadMinedRejects reads Book.MinedRejects and returns the normalized src set the emission excludes (like +// the seed surfaces). Empty path → nil (no rejects). Each src is normalized via normalizeSourceKey so a +// reject matches the miner's normalized candidate surface whichever orthographic form the owner pasted from +// the signature map; a blank src is skipped (a stray list entry must not silently match everything). +func (r *Runner) loadMinedRejects() (map[string]bool, error) { + if r.Book.MinedRejects == "" { + return nil, nil + } + raw, err := os.ReadFile(r.Book.MinedRejects) + if err != nil { + return nil, fmt.Errorf("pipeline: read mined-rejects %s: %w", r.Book.MinedRejects, err) + } + var f minedRejectFile + if err := yaml.Unmarshal(raw, &f); err != nil { + return nil, fmt.Errorf("pipeline: parse mined-rejects %s: %w", r.Book.MinedRejects, err) + } + rejects := map[string]bool{} + for _, rj := range f.Rejects { + if nk := normalizeSourceKey(rj.Src); nk != "" { + rejects[nk] = true + } + } + return rejects, nil +} + // loadMinedDelta reads the owner-curated mined-delta YAML (book.MinedDelta) and stamps every entry // Source:"mined" — NOT via loadGlossarySeed (which hardcodes Source:"seed", memseed.go, moving the base // bank / draft-wave snapshot). This is the mined-write path (plan §1(в), F2): the mined terms land in the ENRICHED diff --git a/backend/internal/pipeline/quality.go b/backend/internal/pipeline/quality.go index 7942ebf2..54c12e05 100644 --- a/backend/internal/pipeline/quality.go +++ b/backend/internal/pipeline/quality.go @@ -15,18 +15,19 @@ import ( // no LLM, no snapshot touch, no checkpoint replay — only OBSERVABILITY, never a gate. The semantic // span-judge (inversion/omission backstop for claim-2) is NOT here — it is research-dependent (пак-2). -// QualityReport is the whole-book per-run quality projection. +// QualityReport is the whole-book per-run quality projection. The shipping granularity is the OUTPUT UNIT +// (edit unit for an edit pipeline, draft chunk for a draft-only one), so the whole-book counters are named +// *_units (naming-debt fix D39.18-follow-up: pre-c-lite they said "chunks" but count units). type QualityReport struct { - BookID string `json:"book_id"` - TotalChunks int `json:"total_chunks"` - // TextChunks is the number of chunks whose exported final text was available for the structural - // KPI (done or cosmetically-stripped); a flagged-empty chunk contributes no prose. - TextChunks int `json:"text_chunks"` - // ProcessedChunks is the number of chunks that REACHED the final stage (a final-stage row exists: - // ok, cosmetic-strip, or skipped-because-upstream-flagged) — the rate denominator, so echo/CJK - // rates are a bounded [0,1] fraction of processed chunks (an echo chunk is a skipped final row, - // disjoint from the exported-text set, which is why dividing by TextChunks alone could exceed 1). - ProcessedChunks int `json:"processed_chunks"` + BookID string `json:"book_id"` + TotalUnits int `json:"total_units"` + // TextUnits is the number of units whose exported final text was available for the structural KPI + // (done or cosmetically-stripped); a flagged-empty unit contributes no prose. + TextUnits int `json:"text_units"` + // ProcessedUnits is the number of units that REACHED the final stage (a final-stage row exists: ok, + // cosmetic-strip, or skipped-because-a-member-flagged) — the strip-rate denominator, so the rate is a + // bounded [0,1] fraction of processed units. + ProcessedUnits int `json:"processed_units"` // Claim-1 structural KPI (рубленые абзацы). MeanSentPerNarrPara ≈ 1 is choppy (one sentence per // paragraph — the owner's exact complaint); higher is merged discourse prose. Aggregated as @@ -36,19 +37,27 @@ type QualityReport struct { MeanSentPerNarrPara float64 `json:"mean_sentences_per_narrative_paragraph"` // Deterministic signal aggregates (all observability, never a disposition). - DialogueDashFlags int `json:"dialogue_dash_flags"` // Rosenthal dialogue-dash inconsistencies - GlossaryMisses int `json:"glossary_misses"` // CONFIRMED post-check misses (D10 consistency) - NumberDriftFlags int `json:"number_drift_flags"` // reflow number drift + 万/億 magnitude drift - TrustGated int `json:"trust_gated"` // lower-trust suppressions refused (seed hygiene) - CosmeticStripChunks int `json:"cosmetic_strip_chunks"` // chunks the sanitizer auto-stripped (markdown header OR CJK leak — F6: was mislabeled cjk_leak) - EchoChunks int `json:"echo_chunks"` // chunks flagged cjk_artifact (untranslated echo) + DialogueDashFlags int `json:"dialogue_dash_flags"` // Rosenthal dialogue-dash inconsistencies + GlossaryMisses int `json:"glossary_misses"` // CONFIRMED post-check misses (D10 consistency) + NumberDriftFlags int `json:"number_drift_flags"` // reflow number drift + 万/億 magnitude drift + TrustGated int `json:"trust_gated"` // lower-trust suppressions refused (seed hygiene) + CosmeticStripUnits int `json:"cosmetic_strip_units"` // units the sanitizer auto-stripped (markdown header OR CJK leak — F6) - // Rates over ProcessedChunks (0..1) for the two "instant unreadability" families, so the numbers - // read as a bounded fraction of processed chunks rather than a bare count. CosmeticStripRate covers - // BOTH strip classes (a markdown-only strip is NOT a CJK leak — F6, D39.4: the old cjk_leak_rate - // counted every sanitizer_stripped chunk and read as false CJK leakage). + // Echo is SPLIT by stage (owner decision, D39.18-follow-up): a translator echo (draft) and an editor + // echo (edit) measure different things and were conflated by the old single echo_rate + a c-lite + // re-derive hack. echo_draft = the DRAFT quality (fraction of draft-stage rows flagged cjk_artifact, + // INCLUDING a c-lite dropped member — the translator echoed even if the editor recovered the unit), + // computed DIRECTLY from the draft rows (no per-unit re-derivation). echo_edit = the DELIVERED quality + // (fraction of edit-stage units whose EDITOR output itself echoed — a skipped edit row is a draft echo, + // not an editor one, so it is excluded from the numerator). + EchoDraftChunks int `json:"echo_draft_chunks"` // draft-stage rows flagged cjk_artifact (per DRAFT chunk) + EchoDraftRate float64 `json:"echo_draft_rate"` // over live draft rows + EchoEditUnits int `json:"echo_edit_units"` // edit-stage units whose editor output echoed + EchoEditRate float64 `json:"echo_edit_rate"` // over live edit rows + + // CosmeticStripRate is over ProcessedUnits (0..1). It covers BOTH strip classes (a markdown-only strip + // is NOT a CJK leak — F6, D39.4: the old cjk_leak_rate counted every sanitizer_stripped unit). CosmeticStripRate float64 `json:"cosmetic_strip_rate"` - EchoRate float64 `json:"echo_rate"` Chunks []ChunkQuality `json:"chunks,omitempty"` } @@ -103,9 +112,9 @@ func (r *Runner) QualityReport() (*QualityReport, error) { return nil, err } // Total = the SHIPPING units (the manifest re-chunk, $0), matching status/export + the per-unit - // BookResult (R1): under the wave model the editor's final text is per EDIT UNIT, so ProcessedChunks - // (the lastStage rows, one per unit leader) and TotalChunks agree at unit granularity. The per-unit - // KPI/echo/strip signals land on the leader's edit row; a non-leader member's ChunkQuality row carries + // BookResult (R1): under the wave model the editor's final text is per EDIT UNIT, so ProcessedUnits + // (the lastStage rows, one per unit leader) and TotalUnits agree at unit granularity. The per-unit + // KPI/strip signals land on the leader's edit row; a non-leader member's ChunkQuality row carries // only its draft-side signals (trust-gated) with a 0 structural KPI — observability, never a gate. chunks, err := r.bookChunks() if err != nil { @@ -113,35 +122,24 @@ func (r *Runner) QualityReport() (*QualityReport, error) { } units := r.outputUnits(chunks) // GHOST guard (parity with Export/Status): a stored row whose unit-leader key is NOT in the current - // manifest (source shrank since the run) is a ghost — dropping it keeps ProcessedChunks ≤ TotalChunks - // and the echo/strip rates over live units only, instead of mixing in stale leader rows. + // manifest (source shrank since the run) is a ghost — dropping it keeps ProcessedUnits ≤ TotalUnits. inManifest := map[chunkKey]bool{} for _, u := range units { inManifest[chunkKey{u.Chapter, u.FirstChunkIdx}] = true } - - // c-lite (D39.17-fix): the wave editor drops a flagged member and edits the clean remainder, so the leader - // edit row is DispOK even though a member's draft echoed/stripped. Re-derive the dropped member's reason per - // unit (like export/status) so the echo/strip RATES still count it — else a unit RECOVERED by editing the - // clean remainder would vanish from EchoRate/CosmeticStripRate the owner reads for the acceptance planka. + // A retrieval_state row / a draft echo is keyed per DRAFT chunk, so it is live iff its chunk is still in + // the manifest; a chunk that left the source is a ghost. + liveChunks := map[chunkKey]bool{} + for _, ch := range chunks { + liveChunks[chunkKey{ch.Chapter, ch.ChunkIdx}] = true + } + // Wave stage-name sets classify a stored chunk_status row by wave for the split echo metric (D39.18 + // owner decision): echo_draft is counted DIRECTLY from the draft rows (no c-lite per-unit re-derivation + // — a dropped member's own draft row already carries cjk_artifact), echo_edit from the edit rows. draftStageNames := stageNameSet(r.waveStagesIndexed(waveDraft)) - memberDraftFlag := map[chunkKey]string{} - for _, cs := range statuses { - if draftStageNames[cs.Stage] && cs.Disposition == string(DispFlagged) { - memberDraftFlag[chunkKey{cs.Chapter, cs.ChunkIdx}] = cs.FlagReason - } - } - droppedReasonByLeader := map[chunkKey]string{} - for _, u := range units { - for _, m := range u.Members { - if reason, dropped := memberDraftFlag[chunkKey{m.Chapter, m.ChunkIdx}]; dropped { - droppedReasonByLeader[chunkKey{u.Chapter, u.FirstChunkIdx}] = reason - break - } - } - } + editStageNames := stageNameSet(r.waveStagesIndexed(waveEdit)) - rep := &QualityReport{BookID: r.Book.BookID, TotalChunks: len(units)} + rep := &QualityReport{BookID: r.Book.BookID, TotalUnits: len(units)} byChunk := map[chunkKey]*ChunkQuality{} order := []chunkKey{} chunkOf := func(k chunkKey) *ChunkQuality { @@ -157,18 +155,13 @@ func (r *Runner) QualityReport() (*QualityReport, error) { // The glossary post-check GATE flips a chunk to withheld (flagged glossary_miss, empty export) at // the CHUNK level without a chunk_status row (like Export/Status re-derive it). F5 (D39.4): the // structural KPI must EXCLUDE these — their export is "", so counting their (withheld) text in - // TextChunks/KPI diverges from what `tmctl export` and `translate` actually ship. + // TextUnits/KPI diverges from what `tmctl export` and `translate` actually ship. gateOn := r.Pipeline.Gates.Glossary.PostcheckGate withheld := map[chunkKey]bool{} // Aggregate the stored retrieval-state signals (glossary consistency, style breakdown, trust-gated). - // A retrieval_state row is keyed per DRAFT chunk, so it is live iff its chunk is still in the manifest; - // a chunk that left the source is a ghost. (A leader row also carries the unit's post-check; if the - // leader survives, so does the unit.) - liveChunks := map[chunkKey]bool{} - for _, ch := range chunks { - liveChunks[chunkKey{ch.Chapter, ch.ChunkIdx}] = true - } + // A retrieval_state row is keyed per DRAFT chunk (live iff its chunk is still in the manifest); a leader + // row also carries the unit's post-check, so if the leader survives, so does the unit. for _, rs := range states { if !liveChunks[chunkKey{rs.Chapter, rs.ChunkIdx}] { continue // ghost retrieval_state row (chunk dropped from source) @@ -194,7 +187,35 @@ func (r *Runner) QualityReport() (*QualityReport, error) { } } - // The exported-text chunks (the FINAL stage's row): echo/CJK flag rates + the structural KPI. + // Split echo by stage (D39.18 owner decision): echo_draft over the DRAFT-stage rows (translator quality, + // INCLUDING a c-lite dropped member — its own draft row carries cjk_artifact, so no per-unit re-derivation + // is needed), echo_edit over the EDIT-stage rows (delivered quality — only the EDITOR's own echo counts, + // a skipped edit row is a draft echo not an editor one). Ghost-guarded like the rest (live chunks / units). + var draftRows, draftEcho, editRows, editEcho int + for _, cs := range statuses { + k := chunkKey{cs.Chapter, cs.ChunkIdx} + switch { + case draftStageNames[cs.Stage] && liveChunks[k]: + draftRows++ + if cs.FlagReason == string(FlagCJKArtifact) { + draftEcho++ // the translator echoed CJK (a flagged draft row), incl. a c-lite dropped member + } + case editStageNames[cs.Stage] && inManifest[k]: + editRows++ + if cs.Disposition == string(DispFlagged) && cs.FlagReason == string(FlagCJKArtifact) { + editEcho++ // the EDITOR's OWN output echoed (a skipped edit row means the drafts echoed, not the editor) + } + } + } + rep.EchoDraftChunks, rep.EchoEditUnits = draftEcho, editEcho + if draftRows > 0 { + rep.EchoDraftRate = float64(draftEcho) / float64(draftRows) + } + if editRows > 0 { + rep.EchoEditRate = float64(editEcho) / float64(editRows) + } + + // The exported-text units (the FINAL stage's row): the cosmetic-strip rate + the structural KPI. lastStage := "" if n := len(r.Pipeline.Stages); n > 0 { lastStage = r.Pipeline.Stages[n-1].Name @@ -204,39 +225,21 @@ func (r *Runner) QualityReport() (*QualityReport, error) { if cs.Stage != lastStage || !inManifest[k] { continue // the final verdict lives on the final stage's row (per unit); drop ghost leader rows } - // Every chunk that REACHED the final stage has exactly one lastStage row (ok, cosmetic-strip, - // or skipped-because-an-upstream-stage-flagged). This is the common rate denominator so the - // echo/CJK rates are a bounded fraction of processed chunks (an echo chunk is a skipped final - // row, disjoint from the exported-text set — dividing by TextChunks alone could exceed 100%). - rep.ProcessedChunks++ - if cs.FlagReason == string(FlagCJKArtifact) { - rep.EchoChunks++ - chunkOf(k) // ensure the chunk row exists even if it has no retrieval-state signals - } + // Every unit that REACHED the final stage has exactly one lastStage row (ok, cosmetic-strip, or + // skipped-because-a-member-flagged) — the strip-rate denominator. + rep.ProcessedUnits++ if cs.FlagReason == string(FlagSanitizerStripped) { - rep.CosmeticStripChunks++ // a stripped chunk carried a markdown OR CJK cosmetic leak (F6) - } - // c-lite: a DispOK leader edit row whose unit dropped a member counts toward the same rates by the - // dropped member's reason — the echo/strip happened in a real (dropped) draft, recovered by editing the - // rest. Disjoint from the two checks above (they need a flagged edit row; this needs a DispOK one). - if cs.Disposition == string(DispOK) { - switch droppedReasonByLeader[k] { - case string(FlagCJKArtifact): - rep.EchoChunks++ - chunkOf(k) - case string(FlagSanitizerStripped): - rep.CosmeticStripChunks++ - } + rep.CosmeticStripUnits++ // a stripped unit carried a markdown OR CJK cosmetic leak (F6) } // Structural KPI: recompute over the exported final text (ok, or the cosmetic-stripped export). if cs.FinalHash == "" { continue } if cs.Disposition != string(DispOK) && cs.FlagReason != string(FlagSanitizerStripped) { - continue // a dropped chunk exported nothing + continue // a dropped unit exported nothing } if withheld[k] { - continue // F5: the glossary gate withheld this chunk's text — it exports nothing + continue // F5: the glossary gate withheld this unit's text — it exports nothing } cp, cperr := r.Store.GetCheckpoint(cs.FinalHash) if cperr != nil { @@ -250,15 +253,14 @@ func (r *Runner) QualityReport() (*QualityReport, error) { q.NarrativeSentences, q.NarrativeParagraphs = sent, para rep.NarrativeSentences += sent rep.NarrativeParagraphs += para - rep.TextChunks++ + rep.TextUnits++ } if rep.NarrativeParagraphs > 0 { rep.MeanSentPerNarrPara = float64(rep.NarrativeSentences) / float64(rep.NarrativeParagraphs) } - if rep.ProcessedChunks > 0 { - rep.CosmeticStripRate = float64(rep.CosmeticStripChunks) / float64(rep.ProcessedChunks) - rep.EchoRate = float64(rep.EchoChunks) / float64(rep.ProcessedChunks) + if rep.ProcessedUnits > 0 { + rep.CosmeticStripRate = float64(rep.CosmeticStripUnits) / float64(rep.ProcessedUnits) } for _, k := range order { rep.Chunks = append(rep.Chunks, *byChunk[k]) diff --git a/backend/internal/pipeline/quality_test.go b/backend/internal/pipeline/quality_test.go index 1c903bce..985d9c89 100644 --- a/backend/internal/pipeline/quality_test.go +++ b/backend/internal/pipeline/quality_test.go @@ -62,12 +62,12 @@ func TestQualityReportAggregates(t *testing.T) { if err != nil { t.Fatalf("QualityReport: %v", err) } - if q.TotalChunks != 1 || q.TextChunks != 1 || q.ProcessedChunks != 1 { - t.Fatalf("chunk counts: total=%d processed=%d text=%d, want 1/1/1", q.TotalChunks, q.ProcessedChunks, q.TextChunks) + if q.TotalUnits != 1 || q.TextUnits != 1 || q.ProcessedUnits != 1 { + t.Fatalf("unit counts: total=%d processed=%d text=%d, want 1/1/1", q.TotalUnits, q.ProcessedUnits, q.TextUnits) } - // Rates are bounded [0,1] over ProcessedChunks (the disjoint-set bug fix). - if q.EchoRate < 0 || q.EchoRate > 1 || q.CosmeticStripRate < 0 || q.CosmeticStripRate > 1 { - t.Errorf("rates must be within [0,1], got echo=%.3f cosmetic=%.3f", q.EchoRate, q.CosmeticStripRate) + // Rates are bounded [0,1]. + if q.EchoDraftRate < 0 || q.EchoDraftRate > 1 || q.EchoEditRate < 0 || q.EchoEditRate > 1 || q.CosmeticStripRate < 0 || q.CosmeticStripRate > 1 { + t.Errorf("rates must be within [0,1], got echo_draft=%.3f echo_edit=%.3f cosmetic=%.3f", q.EchoDraftRate, q.EchoEditRate, q.CosmeticStripRate) } if q.NarrativeSentences != 3 || q.NarrativeParagraphs != 2 { t.Errorf("structural KPI = %d sent / %d para, want 3/2", q.NarrativeSentences, q.NarrativeParagraphs) @@ -75,8 +75,8 @@ func TestQualityReportAggregates(t *testing.T) { if q.MeanSentPerNarrPara != 1.5 { t.Errorf("mean sentences/narrative-paragraph = %.3f, want 1.5", q.MeanSentPerNarrPara) } - // A clean run: no glossary misses, no echo, no CJK strip, no trust-gated, no dialogue-dash issue. - if q.GlossaryMisses != 0 || q.EchoChunks != 0 || q.CosmeticStripChunks != 0 || q.TrustGated != 0 || q.DialogueDashFlags != 0 { + // A clean run: no glossary misses, no echo (draft or edit), no CJK strip, no trust-gated, no dash issue. + if q.GlossaryMisses != 0 || q.EchoDraftChunks != 0 || q.EchoEditUnits != 0 || q.CosmeticStripUnits != 0 || q.TrustGated != 0 || q.DialogueDashFlags != 0 { t.Errorf("clean run must have zero defect signals, got %+v", q) } if len(q.Chunks) != 1 || q.Chunks[0].NarrativeParagraphs != 2 { @@ -106,13 +106,13 @@ func TestQualityReportExcludesGateWithheld(t *testing.T) { if err != nil { t.Fatalf("QualityReport: %v", err) } - // The single chunk is gate-withheld → processed but NOT text-bearing. - if q.ProcessedChunks != 1 { - t.Fatalf("the withheld chunk still reached the final stage: processed=%d", q.ProcessedChunks) + // The single unit is gate-withheld → processed but NOT text-bearing. + if q.ProcessedUnits != 1 { + t.Fatalf("the withheld unit still reached the final stage: processed=%d", q.ProcessedUnits) } - if q.TextChunks != 0 || q.NarrativeSentences != 0 || q.NarrativeParagraphs != 0 { - t.Errorf("gate-withheld chunk must be excluded from the KPI, got text=%d sent=%d para=%d", - q.TextChunks, q.NarrativeSentences, q.NarrativeParagraphs) + if q.TextUnits != 0 || q.NarrativeSentences != 0 || q.NarrativeParagraphs != 0 { + t.Errorf("gate-withheld unit must be excluded from the KPI, got text=%d sent=%d para=%d", + q.TextUnits, q.NarrativeSentences, q.NarrativeParagraphs) } // It IS visible as a glossary miss (the observability signal is kept). if q.GlossaryMisses == 0 { diff --git a/backend/internal/pipeline/runner_readonly_test.go b/backend/internal/pipeline/runner_readonly_test.go index c0dc5b45..1621913c 100644 --- a/backend/internal/pipeline/runner_readonly_test.go +++ b/backend/internal/pipeline/runner_readonly_test.go @@ -42,7 +42,7 @@ func TestReadOnlyRunnerWorksDuringLiveRun(t *testing.T) { if err != nil { t.Fatal(err) } - if rep.TotalChunks != 1 || rep.Done != 1 || rep.Flagged != 0 { + if rep.TotalUnits != 1 || rep.Done != 1 || rep.Flagged != 0 { t.Fatalf("status projection over a live-locked book = %+v", rep) } if rep.CommittedUSD == 0 { @@ -69,7 +69,7 @@ func TestReadOnlyRunnerFirstTouchCreates(t *testing.T) { if err != nil { t.Fatal(err) } - if rep.TotalChunks != 1 || rep.Done != 0 || rep.Pending != 1 { + if rep.TotalUnits != 1 || rep.Done != 0 || rep.Pending != 1 { t.Fatalf("pre-run status = %+v", rep) } if rec.count() != 0 { diff --git a/backend/internal/pipeline/seedlint_test.go b/backend/internal/pipeline/seedlint_test.go index a1c50019..f8a96c86 100644 --- a/backend/internal/pipeline/seedlint_test.go +++ b/backend/internal/pipeline/seedlint_test.go @@ -54,7 +54,7 @@ func TestSeedLintEmittedMinedDelta(t *testing.T) { // the bank-mining reseed. chunks := mchunks(strings.Repeat("龙公来到青茅山。龙公很强。青茅山很高。", 4)) seed := []store.GlossaryEntry{{Src: "青茅山", Dst: "гора Цинмао", Type: "place", Status: "approved", Source: "seed"}} - mined := MineBank(chunks, smallContrast(t), seed, frozenMinerConfig(), testLangPack(t)) + mined := MineBank(chunks, smallContrast(t), seed, nil, frozenMinerConfig(), testLangPack(t)) yamlStr, err := MinedDeltaYAML(mined) if err != nil { t.Fatal(err) diff --git a/backend/internal/pipeline/stagerun.go b/backend/internal/pipeline/stagerun.go index a4da9a68..15bf0889 100644 --- a/backend/internal/pipeline/stagerun.go +++ b/backend/internal/pipeline/stagerun.go @@ -537,6 +537,11 @@ func (r *Runner) baseRequestLog(st config.Stage, ch Chunk, model, reqHash string // telemetry, not the resume authority (chunk_status/checkpoints are), so a failed // write must not abort the pipeline — but a jobs table silently stuck in // running/failed desyncs the status/redrive read-models with zero trace. +// +// NOTE (R1 wave model, D39.17-errata nit): a job row is keyed (book, chapter, stage), so the parallel +// members of a multi-chunk edit unit — and the two waves of one chapter — all THRASH one shared row's +// status. This is advisory-only: NOTHING gates on jobs.status (the read-models resolve state from +// chunk_status/checkpoints, R1 post-landing review §1), so the thrash is cosmetic telemetry, not a bug. func (r *Runner) setJobStatus(ctx context.Context, jobID int64, status string) { if err := r.Store.SetJobStatus(jobID, status); err != nil { r.Log.WarnContext(ctx, "job status update failed (non-fatal; jobs table may lag chunk_status)", diff --git a/backend/internal/pipeline/status.go b/backend/internal/pipeline/status.go index c5c5f332..98fa142f 100644 --- a/backend/internal/pipeline/status.go +++ b/backend/internal/pipeline/status.go @@ -33,17 +33,17 @@ const ( // reason, a pass|attention|fail verdict (exp07 chapter rule) and the chapter's spend. type ChapterPassport struct { Chapter int `json:"chapter"` - ChunksTotal int `json:"chunks_total"` - ChunksDone int `json:"chunks_done"` - ChunksFlagged int `json:"chunks_flagged"` - ChunksInFlight int `json:"chunks_in_progress"` - ChunksPending int `json:"chunks_pending"` + UnitsTotal int `json:"units_total"` + UnitsDone int `json:"units_done"` + UnitsFlagged int `json:"units_flagged"` + UnitsInProgress int `json:"units_in_progress"` + UnitsPending int `json:"units_pending"` StagesSkipped int `json:"stages_skipped"` // per-stage skip count (downstream of a flag) Escalations int `json:"escalations"` PostcheckMisses int `json:"postcheck_misses"` // confirmed post-check misses (retrieval_state) StyleFlags int `json:"style_flags"` // cheap style/number gate hits (observability, not a disposition) WorstFlagReason string `json:"worst_flag_reason,omitempty"` - Verdict string `json:"verdict"` // pass | attention | fail (exp07: 0/1/≥2 flagged chunks) + Verdict string `json:"verdict"` // pass | attention | fail (exp07: 0/1/≥2 flagged units) CostUSD float64 `json:"cost_usd"` } @@ -62,7 +62,7 @@ type StatusReport struct { ConfigDrift bool `json:"config_drift"` CurrentSnapshot string `json:"current_snapshot,omitempty"` - TotalChunks int `json:"total_chunks"` + TotalUnits int `json:"total_units"` Done int `json:"done"` InProgress int `json:"in_progress"` Flagged int `json:"flagged"` @@ -228,7 +228,7 @@ func (r *Runner) Status(ctx context.Context) (*StatusReport, error) { // written as a chunk_status row (every stage stays DispOK). Without accounting for it here, status // would report a gate-flagged unit as done/pass while `tmctl translate` exits 2 (finding #2). gateOn := r.Pipeline.Gates.Glossary.PostcheckGate - rep := &StatusReport{BookID: r.Book.BookID, TotalChunks: len(units)} + rep := &StatusReport{BookID: r.Book.BookID, TotalUnits: len(units)} passports := map[int]*ChapterPassport{} var chapterOrder []int var processedCost float64 // spend of PROCESSED units (done+flagged) — the projection base (finding #7) @@ -240,7 +240,7 @@ func (r *Runner) Status(ctx context.Context) (*StatusReport, error) { passports[u.Chapter] = p chapterOrder = append(chapterOrder, u.Chapter) } - p.ChunksTotal++ + p.UnitsTotal++ leader := chunkKey{u.Chapter, u.FirstChunkIdx} var rows []store.ChunkStatus for _, m := range u.Members { @@ -270,19 +270,19 @@ func (r *Runner) Status(ctx context.Context) (*StatusReport, error) { switch state { case ChunkDone: rep.Done++ - p.ChunksDone++ + p.UnitsDone++ case ChunkFlagged: rep.Flagged++ - p.ChunksFlagged++ + p.UnitsFlagged++ if p.WorstFlagReason == "" || flagReasonSeverity(reason) < flagReasonSeverity(p.WorstFlagReason) { p.WorstFlagReason = reason } case ChunkInProgress: rep.InProgress++ - p.ChunksInFlight++ + p.UnitsInProgress++ case ChunkPending: rep.Pending++ - p.ChunksPending++ + p.UnitsPending++ } if state == ChunkDone || state == ChunkFlagged { processedCost += cost // a fully-attempted unit's cost feeds the projection @@ -295,9 +295,9 @@ func (r *Runner) Status(ctx context.Context) (*StatusReport, error) { for _, n := range chapterOrder { p := passports[n] switch { - case p.ChunksFlagged >= 2: + case p.UnitsFlagged >= 2: p.Verdict = "fail" - case p.ChunksFlagged == 1: + case p.UnitsFlagged == 1: p.Verdict = "attention" default: p.Verdict = "pass" @@ -305,8 +305,8 @@ func (r *Runner) Status(ctx context.Context) (*StatusReport, error) { rep.Chapters = append(rep.Chapters, *p) } - if rep.TotalChunks > 0 { - rep.PercentDone = 100 * float64(rep.Done) / float64(rep.TotalChunks) + if rep.TotalUnits > 0 { + rep.PercentDone = 100 * float64(rep.Done) / float64(rep.TotalUnits) } // Per-wave snapshot drift (finding #3, wave-aware): the rows carry draft-wave snapshot (draft) + edit-wave snapshot @@ -380,7 +380,7 @@ func (r *Runner) Status(ctx context.Context) (*StatusReport, error) { // which would over-estimate (finding #7). The clean average × total is the honest estimate. processed := rep.Done + rep.Flagged if processed > 0 { - rep.ProjectedBookUSD = processedCost / float64(processed) * float64(rep.TotalChunks) + rep.ProjectedBookUSD = processedCost / float64(processed) * float64(rep.TotalUnits) } // ETA (secondary): mean fresh-call throughput × remaining processing. No synthetic bar. @@ -394,7 +394,7 @@ func (r *Runner) Status(ctx context.Context) (*StatusReport, error) { if err != nil { return nil, err } - remaining := rep.TotalChunks - processed + remaining := rep.TotalUnits - processed if processed > 0 && freshCalls > 0 && remaining > 0 { meanMSPerChunk := float64(totalMS) / float64(processed) rep.ETASeconds = meanMSPerChunk * float64(remaining) / 1000 diff --git a/backend/internal/pipeline/status_test.go b/backend/internal/pipeline/status_test.go index cbb012f9..204c479a 100644 --- a/backend/internal/pipeline/status_test.go +++ b/backend/internal/pipeline/status_test.go @@ -51,7 +51,7 @@ func TestStatusAndRedrive(t *testing.T) { if err != nil { t.Fatal(err) } - if rep.TotalChunks != 2 || rep.Done != 1 || rep.Flagged != 1 || rep.Pending != 0 || rep.InProgress != 0 { + if rep.TotalUnits != 2 || rep.Done != 1 || rep.Flagged != 1 || rep.Pending != 0 || rep.InProgress != 0 { t.Fatalf("status counts wrong: %+v", rep) } if math.Abs(rep.PercentDone-50) > 1e-9 { @@ -60,10 +60,10 @@ func TestStatusAndRedrive(t *testing.T) { if len(rep.Chapters) != 2 { t.Fatalf("want 2 chapter passports, got %d", len(rep.Chapters)) } - if rep.Chapters[0].Verdict != "attention" || rep.Chapters[0].WorstFlagReason != string(FlagSoftRefusal) || rep.Chapters[0].ChunksFlagged != 1 { + if rep.Chapters[0].Verdict != "attention" || rep.Chapters[0].WorstFlagReason != string(FlagSoftRefusal) || rep.Chapters[0].UnitsFlagged != 1 { t.Errorf("ch1 passport = %+v (want attention/soft_refusal/1 flagged)", rep.Chapters[0]) } - if rep.Chapters[1].Verdict != "pass" || rep.Chapters[1].ChunksDone != 1 { + if rep.Chapters[1].Verdict != "pass" || rep.Chapters[1].UnitsDone != 1 { t.Errorf("ch2 passport = %+v (want pass/1 done)", rep.Chapters[1]) } wantCommitted := 3 * fakeCallUSD diff --git a/backend/internal/pipeline/testdata/golden/capture.golden b/backend/internal/pipeline/testdata/golden/capture.golden index 3817cc6b..d7cd7941 100644 --- a/backend/internal/pipeline/testdata/golden/capture.golden +++ b/backend/internal/pipeline/testdata/golden/capture.golden @@ -1,11 +1,11 @@ ==== run 1 (fresh) ==== -snapshot_draft: 914c9434d646ec173dbafbac5f2355d9bd4920bf9e3499f237a97c046d82c84d -snapshot_draft_payload: {"brief_hash":"c2021b1b5e6c20f6084e425b9a70bdc5f682801be347a57eb1c591deb8cab0df","chunker_version":"chunker-v5-output-budget-editunit+u15.0.0","estimator_version":"estimator-v0+u15.0.0","max_tokens_policy":"maxtok-v1-double-per-attempt","classifier_version":"classify-v1-refusal+echo015+loop+u15.0.0","pipeline_core":"C1","max_output_ratio":2,"min_max_tokens":512,"context_assembly":{"glossary_injection":"selective","glossary_token_budget":800,"cache_ttl":""},"segmentation":{"draft_budget_out":1797,"edit_ceiling_out":3200,"fertility_cjk":1.1978,"fertility_other":0.3852},"render_format_version":"renderfmt-v2-editor-src2dst+dc3-gender","memory_version":"173e87fc011e832c108711097eeffc05db95ab56eafe2ef913b43829505cdfdd","postcheck_gate":false,"coverage":{"enabled":false},"style_check_version":"cheapgate-v3-dc-checkers+u15.0.0","sanitizer":{"enabled":true,"version":"sanitizer-v6"},"stages":[{"name":"draft","role":"translator","model":"fake-model","prompt_version":"v-golden","prompt_sha256":"26d0245a56d180eafd200ebe5311aab2488142c4a98dcddee21ea457814aa21a","temperature":0.3,"reasoning":"off","capability":{"Budget":"max_tokens","Temp":"send","TempValue":0,"Reasoning":{"Control":"none","OffEffort":"","OffExtraBody":null,"OnExtraBody":null},"MinMaxTokens":4000},"escalate_to":"fake-fallback","escalate_capability":{"Budget":"max_tokens","Temp":"send","TempValue":0,"Reasoning":{"Control":"none","OffEffort":"","OffExtraBody":null,"OnExtraBody":null},"MinMaxTokens":6000}}]} -snapshot_edit: 6fe7e9463b2bb3a93c602468201d650ffcc489c02042099351ac8e66be6f17ed -snapshot_edit_payload: {"brief_hash":"c2021b1b5e6c20f6084e425b9a70bdc5f682801be347a57eb1c591deb8cab0df","chunker_version":"chunker-v5-output-budget-editunit+u15.0.0","estimator_version":"estimator-v0+u15.0.0","max_tokens_policy":"maxtok-v1-double-per-attempt","classifier_version":"classify-v1-refusal+echo015+loop+u15.0.0","pipeline_core":"C1","max_output_ratio":2,"min_max_tokens":512,"context_assembly":{"glossary_injection":"selective","glossary_token_budget":800,"cache_ttl":""},"segmentation":{"draft_budget_out":1797,"edit_ceiling_out":3200,"fertility_cjk":1.1978,"fertility_other":0.3852},"render_format_version":"renderfmt-v2-editor-src2dst+dc3-gender","memory_version":"740aefe880a34396172775751d41d0e9ef136d053f3200a4f94391fa72aee7e4","postcheck_gate":false,"coverage":{"enabled":false},"style_check_version":"cheapgate-v3-dc-checkers+u15.0.0","sanitizer":{"enabled":true,"version":"sanitizer-v6"},"stages":[{"name":"edit","role":"editor","model":"fake-model","prompt_version":"v-golden","prompt_sha256":"6a8e7f71139d6bc09a95159d1cb895e05b56d2f2f7417c5e20a7073c517c79f6","temperature":0.4,"reasoning":"off","capability":{"Budget":"max_tokens","Temp":"send","TempValue":0,"Reasoning":{"Control":"none","OffEffort":"","OffExtraBody":null,"OnExtraBody":null},"MinMaxTokens":4000}}]} +snapshot_draft: b1279edc077f8f26c67e4e94c01eb29e02e0f706d4d18f267519bbb58f2e474a +snapshot_draft_payload: {"brief_hash":"c2021b1b5e6c20f6084e425b9a70bdc5f682801be347a57eb1c591deb8cab0df","chunker_version":"chunker-v5-output-budget-editunit+u15.0.0","estimator_version":"estimator-v0+u15.0.0","max_tokens_policy":"maxtok-v1-double-per-attempt","classifier_version":"classify-v1-refusal+echo015+loop+u15.0.0","pipeline_core":"C1","max_output_ratio":2,"min_max_tokens":512,"context_assembly":{"glossary_injection":"selective","glossary_token_budget":800,"cache_ttl":""},"segmentation":{"draft_budget_out":1797,"edit_ceiling_out":3200,"fertility_cjk":1.1978,"fertility_other":0.3852},"render_format_version":"renderfmt-v2-editor-src2dst+dc3-gender","memory_version":"bd451833d4ebb8a0ca07caf8deda76f027e1f42b78654987a22d215acf955eb7","postcheck_gate":false,"coverage":{"enabled":false},"style_check_version":"cheapgate-v3-dc-checkers+u15.0.0","sanitizer":{"enabled":true,"version":"sanitizer-v6"},"stages":[{"name":"draft","role":"translator","model":"fake-model","prompt_version":"v-golden","prompt_sha256":"26d0245a56d180eafd200ebe5311aab2488142c4a98dcddee21ea457814aa21a","temperature":0.3,"reasoning":"off","capability":{"Budget":"max_tokens","Temp":"send","TempValue":0,"Reasoning":{"Control":"none","OffEffort":"","OffExtraBody":null,"OnExtraBody":null},"MinMaxTokens":4000},"escalate_to":"fake-fallback","escalate_capability":{"Budget":"max_tokens","Temp":"send","TempValue":0,"Reasoning":{"Control":"none","OffEffort":"","OffExtraBody":null,"OnExtraBody":null},"MinMaxTokens":6000}}]} +snapshot_edit: 66a990322f96d7f8f6d4783c63dbee75833b532e31d6a95d2de81394880d0a41 +snapshot_edit_payload: {"brief_hash":"c2021b1b5e6c20f6084e425b9a70bdc5f682801be347a57eb1c591deb8cab0df","chunker_version":"chunker-v5-output-budget-editunit+u15.0.0","estimator_version":"estimator-v0+u15.0.0","max_tokens_policy":"maxtok-v1-double-per-attempt","classifier_version":"classify-v1-refusal+echo015+loop+u15.0.0","pipeline_core":"C1","max_output_ratio":2,"min_max_tokens":512,"context_assembly":{"glossary_injection":"selective","glossary_token_budget":800,"cache_ttl":""},"segmentation":{"draft_budget_out":1797,"edit_ceiling_out":3200,"fertility_cjk":1.1978,"fertility_other":0.3852},"render_format_version":"renderfmt-v2-editor-src2dst+dc3-gender","memory_version":"0a48d6ed9409de09f514af91aebdd622637621c9dc9464e8898c238791033d91","postcheck_gate":false,"coverage":{"enabled":false},"style_check_version":"cheapgate-v3-dc-checkers+u15.0.0","sanitizer":{"enabled":true,"version":"sanitizer-v6"},"stages":[{"name":"edit","role":"editor","model":"fake-model","prompt_version":"v-golden","prompt_sha256":"6a8e7f71139d6bc09a95159d1cb895e05b56d2f2f7417c5e20a7073c517c79f6","temperature":0.4,"reasoning":"off","capability":{"Budget":"max_tokens","Temp":"send","TempValue":0,"Reasoning":{"Control":"none","OffEffort":"","OffExtraBody":null,"OnExtraBody":null},"MinMaxTokens":4000}}]} brief_hash: c2021b1b5e6c20f6084e425b9a70bdc5f682801be347a57eb1c591deb8cab0df -memory_version: 740aefe880a34396172775751d41d0e9ef136d053f3200a4f94391fa72aee7e4 -base_memory_version: 173e87fc011e832c108711097eeffc05db95ab56eafe2ef913b43829505cdfdd +memory_version: 0a48d6ed9409de09f514af91aebdd622637621c9dc9464e8898c238791033d91 +base_memory_version: bd451833d4ebb8a0ca07caf8deda76f027e1f42b78654987a22d215acf955eb7 -- book result -- chunks=8 flagged=4 exit=2 total_usd=0.04003999999999999 chunk ch1/0 disposition=ok flag="" final_text="ОТРЕДАКТИРОВАННЫЙ ПЕРЕВОД 22c30f20dea6. Судзуки шёл по коридорам Академии магии." cost=0.00546 @@ -51,42 +51,42 @@ chunk ch8/0 disposition=flagged flag="sanitizer_stripped" final_text="Судзу stage=edit role=editor model=fake-model resume=false disp=flagged flag="sanitizer_stripped" attempts=1 escalated=false esc_model="" finish="stop" cum_usd=0.00182 detail="CJK-утечка в ru-выходе: 特产" stage_text="" recovered="Судзуки нашёл древний камень на каменном алтаре." -- chunk_status -- -ch1/0 draft snap_match=true content_hash=331c6f32c7f3ed15447e2639a2621d24a180e561a2aee92115324f492db5de3e disp=ok flag="" attempts=1 final_hash=96f55c8caeec48506a73141758fe8e819c9da32c9f6d969e02b83057cc2a7af3 cost=0.00182 escalated=false esc_model="" detail="" -ch1/0 edit snap_match=true content_hash=8a4e763f79c94ad20a1bac0231a8c1376ccd4f5a71c53cdfcb4de146c0b48b42 disp=ok flag="" attempts=1 final_hash=dd9978846ec37ddde3e4db428582661271fd51e8cccbce1374fd2200800e9614 cost=0.00182 escalated=false esc_model="" detail="" -ch1/1 draft snap_match=true content_hash=12fe3ccdf062f43e011363a33ff85c9b6842a8f23d790991fe9afab51a8becc8 disp=ok flag="" attempts=1 final_hash=c9b9807b0279cb186cfae7e1255b2ea6e608b994b3429df9e7cb48f40c21b520 cost=0.00182 escalated=false esc_model="" detail="" -ch2/0 draft snap_match=true content_hash=2693325bc747687acf090bc263fe8c17783726b949c6d8a2d072da3401be6827 disp=ok flag="" attempts=1 final_hash=999ab8cd87c3505efaade5cc30b7f2d38a4e3fc6183250f34906e5c2db84f078 cost=0.00728 escalated=true esc_model="fake-fallback" detail="" -ch2/0 edit snap_match=true content_hash=5a5397d41bcfa9326b5fe67c70aca8d9ef14ef89af2dbe1a758e25d364226fc5 disp=ok flag="" attempts=1 final_hash=894afeaecfc7bef655d937d01a1c69b920c9500d10e4de9d1095f4391a55adac cost=0.00182 escalated=false esc_model="" detail="" +ch1/0 draft snap_match=true content_hash=331c6f32c7f3ed15447e2639a2621d24a180e561a2aee92115324f492db5de3e disp=ok flag="" attempts=1 final_hash=365abc01adb22d047a3865c79231c2669f7db960fd07069d46547d562cfc537b cost=0.00182 escalated=false esc_model="" detail="" +ch1/0 edit snap_match=true content_hash=8a4e763f79c94ad20a1bac0231a8c1376ccd4f5a71c53cdfcb4de146c0b48b42 disp=ok flag="" attempts=1 final_hash=42e10469327dcf7d8409cda457a7eff626795a709ff0b37f3de260d7ca11986a cost=0.00182 escalated=false esc_model="" detail="" +ch1/1 draft snap_match=true content_hash=12fe3ccdf062f43e011363a33ff85c9b6842a8f23d790991fe9afab51a8becc8 disp=ok flag="" attempts=1 final_hash=6f883d099cd0924eab5db1ef3eb730aa50429e719ffeea95df93e33042e9fcc0 cost=0.00182 escalated=false esc_model="" detail="" +ch2/0 draft snap_match=true content_hash=2693325bc747687acf090bc263fe8c17783726b949c6d8a2d072da3401be6827 disp=ok flag="" attempts=1 final_hash=f7a858a11f0890da139789d27bea23b1c33c83ef989f2d07dc51d4b238d5e16e cost=0.00728 escalated=true esc_model="fake-fallback" detail="" +ch2/0 edit snap_match=true content_hash=5a5397d41bcfa9326b5fe67c70aca8d9ef14ef89af2dbe1a758e25d364226fc5 disp=ok flag="" attempts=1 final_hash=ff45d79e2d08b57328491e1ac5e0b6b67ba883346c44806b7a63e7adddcf65bf cost=0.00182 escalated=false esc_model="" detail="" ch3/0 draft snap_match=true content_hash=733fc6a89215efe112cfadd1d1fb361bb8a7ab1013994248a49da0dbd137548e disp=flagged flag="hard_refusal" attempts=1 final_hash= cost=0.00728 escalated=true esc_model="fake-fallback" detail="provider finish_reason=refusal" ch3/0 edit snap_match=true content_hash= disp=skipped flag="hard_refusal" attempts=0 final_hash= cost=0 escalated=false esc_model="" detail="skipped: a member draft chunk of this edit unit was flagged (hard_refusal)" -ch4/0 draft snap_match=true content_hash=7aabdcb89dea365c95363b2662144ca1a10b307d377f47a8e7cc19b5b872d287 disp=ok flag="" attempts=1 final_hash=ac14e02f697c4fc4963946365a118afb5e8ba974c0d3c6781aac17d403dec5fd cost=0.00182 escalated=false esc_model="" detail="" -ch4/0 edit snap_match=true content_hash=2854dfe1648b0cf0cbb683cb7ce94c5eedc9223b2ce20c99dc008c7add6317ef disp=ok flag="" attempts=1 final_hash=5db400bde72cc0e7991f5860a524a73db4b511c683ef918cad505ffba3a6e314 cost=0.00182 escalated=false esc_model="" detail="" -ch5/0 draft snap_match=true content_hash=d8aba42cabb41853a81875ebc725bb516dc2412e29de3a03bc1a68e90f8cf157 disp=ok flag="" attempts=1 final_hash=7a7a6a7c55f5dcd00a80d04e8da5b4bc0f4b9bca8a33baa24397b7dfa9424e20 cost=0.00182 escalated=false esc_model="" detail="" -ch5/0 edit snap_match=true content_hash=82b05e4c3f5ca8432d7eaa7d7d63c4a7766f47a08398953c9a9bc3dc8437039b disp=ok flag="" attempts=1 final_hash=3fdb393a5e9bcba9869c7e0125d89e07f8bf64bb33c0098d999a9dcf5f5285ae cost=0.00182 escalated=false esc_model="" detail="" -ch6/0 draft snap_match=true content_hash=9375b05780724cc354eba15a595da9a5f964219c0887042484256eed11fb566f disp=ok flag="" attempts=1 final_hash=a96545d035e16b72a6abce2274f2ea4664399da63b9aaf88e5dfb3a96af20e2d cost=0.00182 escalated=false esc_model="" detail="" +ch4/0 draft snap_match=true content_hash=7aabdcb89dea365c95363b2662144ca1a10b307d377f47a8e7cc19b5b872d287 disp=ok flag="" attempts=1 final_hash=6fb9e932865b46557a1f5e4a0852a6fe5b2ed728e54d5704801840f1dbe89ccb cost=0.00182 escalated=false esc_model="" detail="" +ch4/0 edit snap_match=true content_hash=2854dfe1648b0cf0cbb683cb7ce94c5eedc9223b2ce20c99dc008c7add6317ef disp=ok flag="" attempts=1 final_hash=5b1f425d363f501da5832d570ebeb036a8d780378abf3e92cdded87c247b9b6e cost=0.00182 escalated=false esc_model="" detail="" +ch5/0 draft snap_match=true content_hash=d8aba42cabb41853a81875ebc725bb516dc2412e29de3a03bc1a68e90f8cf157 disp=ok flag="" attempts=1 final_hash=03ed6d252620e8763a0afc2d535e1b6e1cf97ca1145a66aa08ea199ef7102c04 cost=0.00182 escalated=false esc_model="" detail="" +ch5/0 edit snap_match=true content_hash=82b05e4c3f5ca8432d7eaa7d7d63c4a7766f47a08398953c9a9bc3dc8437039b disp=ok flag="" attempts=1 final_hash=3367114609df41762ff6f64b57de90f85b6f1992987ebf875bc64e028a6a0a4b cost=0.00182 escalated=false esc_model="" detail="" +ch6/0 draft snap_match=true content_hash=9375b05780724cc354eba15a595da9a5f964219c0887042484256eed11fb566f disp=ok flag="" attempts=1 final_hash=1efa0b7620e14fa8cdff89a198efe113e2196a4a48717b1dfacfc9bc878d2ae8 cost=0.00182 escalated=false esc_model="" detail="" ch6/0 edit snap_match=true content_hash=592353321cc8000e792e0897446e4dd4f88a0280b9368b62cbd5c82b0c433193 disp=flagged flag="sanitizer_defect" attempts=1 final_hash= cost=0.00182 escalated=false esc_model="" detail="ведущая служебная преамбула: Вот перевод фрагмента:" -ch7/0 draft snap_match=true content_hash=d90c335cba3d1547a22bb3f656443cb432f84c12a577d43509cfe472cf86a2ac disp=ok flag="" attempts=1 final_hash=9af233395ddde718f61b1bbb4709095a43ea3c4e146ca10990e821607b92bca5 cost=0.00182 escalated=false esc_model="" detail="" -ch7/0 edit snap_match=true content_hash=e12521b6eb6dbc2cd35758d664d4d31894c528f092083b5cfe0e79c51be2663e disp=flagged flag="sanitizer_stripped" attempts=1 final_hash=tm-sanitized-v1:6f0567f25e056974a065d6ea5aedc9b204eeaa7ae7648bd7f3bf34366348355e cost=0.00182 escalated=false esc_model="" detail="markdown-заголовок в выходе: ### Глава 7" -ch8/0 draft snap_match=true content_hash=fbd27af11da2275cd1d47a0224970a5f8d073c6164f614efb6522ea96fa1e021 disp=ok flag="" attempts=1 final_hash=f5da217f1c2a26da0f7b7d946dada785e03cb30dc13600272cf74b0c54704451 cost=0.00182 escalated=false esc_model="" detail="" -ch8/0 edit snap_match=true content_hash=0c6030a943cb341390509e2ebed4bbfd52af328d36916ce2e341f43665cf9183 disp=flagged flag="sanitizer_stripped" attempts=1 final_hash=tm-sanitized-v1:e0ea15352185a07b28f7b8b2ca50567d1b8044c5d8ad54ad6fd9d3e45f761c51 cost=0.00182 escalated=false esc_model="" detail="CJK-утечка в ru-выходе: 特产" +ch7/0 draft snap_match=true content_hash=d90c335cba3d1547a22bb3f656443cb432f84c12a577d43509cfe472cf86a2ac disp=ok flag="" attempts=1 final_hash=3a1050fe790ac5eb004c8d3e80a05cc6ff5e6b46c5947f6a57a7145985591dfb cost=0.00182 escalated=false esc_model="" detail="" +ch7/0 edit snap_match=true content_hash=e12521b6eb6dbc2cd35758d664d4d31894c528f092083b5cfe0e79c51be2663e disp=flagged flag="sanitizer_stripped" attempts=1 final_hash=tm-sanitized-v1:e9a837dbcd8d6717d27413735debe11bc116df2f65655d16f6fcbf51cbf59e66 cost=0.00182 escalated=false esc_model="" detail="markdown-заголовок в выходе: ### Глава 7" +ch8/0 draft snap_match=true content_hash=fbd27af11da2275cd1d47a0224970a5f8d073c6164f614efb6522ea96fa1e021 disp=ok flag="" attempts=1 final_hash=e1e065fc34385f41fd51a4b0db42d5f391532775ef68ece015f261a33d6b68da cost=0.00182 escalated=false esc_model="" detail="" +ch8/0 edit snap_match=true content_hash=0c6030a943cb341390509e2ebed4bbfd52af328d36916ce2e341f43665cf9183 disp=flagged flag="sanitizer_stripped" attempts=1 final_hash=tm-sanitized-v1:08f107da1257ff083f73e9af1e8d3b5b90e699ad6343faa80bebe891744c7adf cost=0.00182 escalated=false esc_model="" detail="CJK-утечка в ru-выходе: 特产" -- request_log (insertion order) -- -ch1/0 draft role=translator req=fake-model actual=fake-model hash=96f55c8caeec48506a73141758fe8e819c9da32c9f6d969e02b83057cc2a7af3 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch1/1 draft role=translator req=fake-model actual=fake-model hash=c9b9807b0279cb186cfae7e1255b2ea6e608b994b3429df9e7cb48f40c21b520 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch2/0 draft role=translator req=fake-model actual=fake-model hash=2e498c5ae088fcd6701b94c2f2ad7761033b6f663a26a48554ae12ca2c2f2630 tm_hit=0 ok=0 finish="stop" degraded="cjk_artifact" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch2/0 draft role=translator req=fake-fallback actual=fake-fallback hash=999ab8cd87c3505efaade5cc30b7f2d38a4e3fc6183250f34906e5c2db84f078 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.0054600000000000004 tokens=1000/200/0/500/0 err="" -ch3/0 draft role=translator req=fake-model actual=fake-model hash=6b744aad02e6e2d255353035a3943a278bc407cf2b1f2abaac9503c0d21239da tm_hit=0 ok=0 finish="refusal" degraded="hard_refusal" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch3/0 draft role=translator req=fake-fallback actual=fake-fallback hash=c42d577ea9c432be30ea7716f15b779320113de3d1269559988fde390fe1589d tm_hit=0 ok=0 finish="refusal" degraded="hard_refusal" cost=0.0054600000000000004 tokens=1000/200/0/500/0 err="" -ch4/0 draft role=translator req=fake-model actual=fake-model hash=ac14e02f697c4fc4963946365a118afb5e8ba974c0d3c6781aac17d403dec5fd tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch5/0 draft role=translator req=fake-model actual=fake-model hash=7a7a6a7c55f5dcd00a80d04e8da5b4bc0f4b9bca8a33baa24397b7dfa9424e20 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch6/0 draft role=translator req=fake-model actual=fake-model hash=a96545d035e16b72a6abce2274f2ea4664399da63b9aaf88e5dfb3a96af20e2d tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch7/0 draft role=translator req=fake-model actual=fake-model hash=9af233395ddde718f61b1bbb4709095a43ea3c4e146ca10990e821607b92bca5 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch8/0 draft role=translator req=fake-model actual=fake-model hash=f5da217f1c2a26da0f7b7d946dada785e03cb30dc13600272cf74b0c54704451 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch1/0 edit role=editor req=fake-model actual=fake-model hash=dd9978846ec37ddde3e4db428582661271fd51e8cccbce1374fd2200800e9614 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch2/0 edit role=editor req=fake-model actual=fake-model hash=894afeaecfc7bef655d937d01a1c69b920c9500d10e4de9d1095f4391a55adac tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch4/0 edit role=editor req=fake-model actual=fake-model hash=5db400bde72cc0e7991f5860a524a73db4b511c683ef918cad505ffba3a6e314 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch5/0 edit role=editor req=fake-model actual=fake-model hash=3fdb393a5e9bcba9869c7e0125d89e07f8bf64bb33c0098d999a9dcf5f5285ae tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch6/0 edit role=editor req=fake-model actual=fake-model hash=8db26b560abdf5e538830d6e60d6552225140163668ee698936990ceba379189 tm_hit=0 ok=0 finish="stop" degraded="sanitizer_defect" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch7/0 edit role=editor req=fake-model actual=fake-model hash=9ba86bea5f00ed45d089fea42c4353ed4321ade3980514765e26cf891aedd319 tm_hit=0 ok=0 finish="stop" degraded="sanitizer_stripped" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch8/0 edit role=editor req=fake-model actual=fake-model hash=943591a7303b83478ab8f24af256c4e663b7b01f9fed670a704ae10a330798f8 tm_hit=0 ok=0 finish="stop" degraded="sanitizer_stripped" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch1/0 draft role=translator req=fake-model actual=fake-model hash=365abc01adb22d047a3865c79231c2669f7db960fd07069d46547d562cfc537b tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch1/1 draft role=translator req=fake-model actual=fake-model hash=6f883d099cd0924eab5db1ef3eb730aa50429e719ffeea95df93e33042e9fcc0 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch2/0 draft role=translator req=fake-model actual=fake-model hash=0932d7aee2cd8fe944a4f5fcc4bf5b1828d8514ecc46cde7b5a3bd9983864dd2 tm_hit=0 ok=0 finish="stop" degraded="cjk_artifact" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch2/0 draft role=translator req=fake-fallback actual=fake-fallback hash=f7a858a11f0890da139789d27bea23b1c33c83ef989f2d07dc51d4b238d5e16e tm_hit=0 ok=1 finish="stop" degraded="" cost=0.0054600000000000004 tokens=1000/200/0/500/0 err="" +ch3/0 draft role=translator req=fake-model actual=fake-model hash=7be9635f28275d919b1ff3ff6aab9f94039eff250163bef3d6d3460489984b7e tm_hit=0 ok=0 finish="refusal" degraded="hard_refusal" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch3/0 draft role=translator req=fake-fallback actual=fake-fallback hash=ef672cdf6862522d15d93c32faf95d7eebc2c6ff4c24de6b0679a256900349ac tm_hit=0 ok=0 finish="refusal" degraded="hard_refusal" cost=0.0054600000000000004 tokens=1000/200/0/500/0 err="" +ch4/0 draft role=translator req=fake-model actual=fake-model hash=6fb9e932865b46557a1f5e4a0852a6fe5b2ed728e54d5704801840f1dbe89ccb tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch5/0 draft role=translator req=fake-model actual=fake-model hash=03ed6d252620e8763a0afc2d535e1b6e1cf97ca1145a66aa08ea199ef7102c04 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch6/0 draft role=translator req=fake-model actual=fake-model hash=1efa0b7620e14fa8cdff89a198efe113e2196a4a48717b1dfacfc9bc878d2ae8 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch7/0 draft role=translator req=fake-model actual=fake-model hash=3a1050fe790ac5eb004c8d3e80a05cc6ff5e6b46c5947f6a57a7145985591dfb tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch8/0 draft role=translator req=fake-model actual=fake-model hash=e1e065fc34385f41fd51a4b0db42d5f391532775ef68ece015f261a33d6b68da tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch1/0 edit role=editor req=fake-model actual=fake-model hash=42e10469327dcf7d8409cda457a7eff626795a709ff0b37f3de260d7ca11986a tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch2/0 edit role=editor req=fake-model actual=fake-model hash=ff45d79e2d08b57328491e1ac5e0b6b67ba883346c44806b7a63e7adddcf65bf tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch4/0 edit role=editor req=fake-model actual=fake-model hash=5b1f425d363f501da5832d570ebeb036a8d780378abf3e92cdded87c247b9b6e tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch5/0 edit role=editor req=fake-model actual=fake-model hash=3367114609df41762ff6f64b57de90f85b6f1992987ebf875bc64e028a6a0a4b tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch6/0 edit role=editor req=fake-model actual=fake-model hash=db25b5d375286a9fa18bc3b7b9da14c8bd95a5209b8b2e9099889413062504d1 tm_hit=0 ok=0 finish="stop" degraded="sanitizer_defect" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch7/0 edit role=editor req=fake-model actual=fake-model hash=54ab4a1ffb853422a3b079b71b3c1c7ba11eb434c3d9f1df3c81aec2f007e0d8 tm_hit=0 ok=0 finish="stop" degraded="sanitizer_stripped" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch8/0 edit role=editor req=fake-model actual=fake-model hash=c047ff047484181ff697d1e525c3d12f5b76ab5f9451cc5f309be3465559cf5e tm_hit=0 ok=0 finish="stop" degraded="sanitizer_stripped" cost=0.00182 tokens=1000/200/0/500/0 err="" -- retrieval_state -- ch1/0 snap_match=true exact=3 sticky=0 ambiguous=0 spoiler=1 evicted=0 postcheck_miss=1 style_flags=0 trust_gated=0 injected_ids=["紋章\u001f\u001f0\u001f0","鈴木\u001f\u001f0\u001f0","魔法学院\u001f\u001f0\u001f0"] postcheck_detail=[{"src":"紋章","dst":"герб","disp":"confirmed"}] style_detail= trust_gate_detail= @@ -126,13 +126,13 @@ ch8/0 snap_match=true exact=1 sticky=0 ambiguous=0 spoiler=0 evicted=0 postcheck [16] {"max_tokens":4000,"messages":[{"role":"system","content":"Ты — монолингвальный редактор русского текста. Правь стиль, не меняя смысла. Книга: Золотая книга."},{"role":"system","content":"КАНОНИЧЕСКИЕ ПЕРЕВОДЫ имён и терминов (в черновике термин исходника слева ДОЛЖЕН быть передан именно указанной формой справа — приводи к ней любые расхождения, склоняя по контексту; не вводи иных вариантов и не меняй ничего другого):\n- 鈴木 → «Судзуки» (муж. — мужские родовые формы)"},{"role":"user","content":"Черновик перевода для редактуры: ЧЕРНОВИК ПЕРЕВОДА ec12f3d7a1df. МАРКДАУН-СЦЕНА Судзуки открыл седьмую дверь."}],"model":"fake-model","stream":false,"temperature":0.4} [17] {"max_tokens":4000,"messages":[{"role":"system","content":"Ты — монолингвальный редактор русского текста. Правь стиль, не меняя смысла. Книга: Золотая книга."},{"role":"system","content":"КАНОНИЧЕСКИЕ ПЕРЕВОДЫ имён и терминов (в черновике термин исходника слева ДОЛЖЕН быть передан именно указанной формой справа — приводи к ней любые расхождения, склоняя по контексту; не вводи иных вариантов и не меняй ничего другого):\n- 鈴木 → «Судзуки» (муж. — мужские родовые формы)"},{"role":"user","content":"Черновик перевода для редактуры: ЧЕРНОВИК ПЕРЕВОДА 0bc6a4817acb. ИЕРОГЛИФ-СЦЕНА Судзуки нашёл камень."}],"model":"fake-model","stream":false,"temperature":0.4} ==== run 2 (resume) ==== -snapshot_draft: 914c9434d646ec173dbafbac5f2355d9bd4920bf9e3499f237a97c046d82c84d -snapshot_draft_payload: {"brief_hash":"c2021b1b5e6c20f6084e425b9a70bdc5f682801be347a57eb1c591deb8cab0df","chunker_version":"chunker-v5-output-budget-editunit+u15.0.0","estimator_version":"estimator-v0+u15.0.0","max_tokens_policy":"maxtok-v1-double-per-attempt","classifier_version":"classify-v1-refusal+echo015+loop+u15.0.0","pipeline_core":"C1","max_output_ratio":2,"min_max_tokens":512,"context_assembly":{"glossary_injection":"selective","glossary_token_budget":800,"cache_ttl":""},"segmentation":{"draft_budget_out":1797,"edit_ceiling_out":3200,"fertility_cjk":1.1978,"fertility_other":0.3852},"render_format_version":"renderfmt-v2-editor-src2dst+dc3-gender","memory_version":"173e87fc011e832c108711097eeffc05db95ab56eafe2ef913b43829505cdfdd","postcheck_gate":false,"coverage":{"enabled":false},"style_check_version":"cheapgate-v3-dc-checkers+u15.0.0","sanitizer":{"enabled":true,"version":"sanitizer-v6"},"stages":[{"name":"draft","role":"translator","model":"fake-model","prompt_version":"v-golden","prompt_sha256":"26d0245a56d180eafd200ebe5311aab2488142c4a98dcddee21ea457814aa21a","temperature":0.3,"reasoning":"off","capability":{"Budget":"max_tokens","Temp":"send","TempValue":0,"Reasoning":{"Control":"none","OffEffort":"","OffExtraBody":null,"OnExtraBody":null},"MinMaxTokens":4000},"escalate_to":"fake-fallback","escalate_capability":{"Budget":"max_tokens","Temp":"send","TempValue":0,"Reasoning":{"Control":"none","OffEffort":"","OffExtraBody":null,"OnExtraBody":null},"MinMaxTokens":6000}}]} -snapshot_edit: 6fe7e9463b2bb3a93c602468201d650ffcc489c02042099351ac8e66be6f17ed -snapshot_edit_payload: {"brief_hash":"c2021b1b5e6c20f6084e425b9a70bdc5f682801be347a57eb1c591deb8cab0df","chunker_version":"chunker-v5-output-budget-editunit+u15.0.0","estimator_version":"estimator-v0+u15.0.0","max_tokens_policy":"maxtok-v1-double-per-attempt","classifier_version":"classify-v1-refusal+echo015+loop+u15.0.0","pipeline_core":"C1","max_output_ratio":2,"min_max_tokens":512,"context_assembly":{"glossary_injection":"selective","glossary_token_budget":800,"cache_ttl":""},"segmentation":{"draft_budget_out":1797,"edit_ceiling_out":3200,"fertility_cjk":1.1978,"fertility_other":0.3852},"render_format_version":"renderfmt-v2-editor-src2dst+dc3-gender","memory_version":"740aefe880a34396172775751d41d0e9ef136d053f3200a4f94391fa72aee7e4","postcheck_gate":false,"coverage":{"enabled":false},"style_check_version":"cheapgate-v3-dc-checkers+u15.0.0","sanitizer":{"enabled":true,"version":"sanitizer-v6"},"stages":[{"name":"edit","role":"editor","model":"fake-model","prompt_version":"v-golden","prompt_sha256":"6a8e7f71139d6bc09a95159d1cb895e05b56d2f2f7417c5e20a7073c517c79f6","temperature":0.4,"reasoning":"off","capability":{"Budget":"max_tokens","Temp":"send","TempValue":0,"Reasoning":{"Control":"none","OffEffort":"","OffExtraBody":null,"OnExtraBody":null},"MinMaxTokens":4000}}]} +snapshot_draft: b1279edc077f8f26c67e4e94c01eb29e02e0f706d4d18f267519bbb58f2e474a +snapshot_draft_payload: {"brief_hash":"c2021b1b5e6c20f6084e425b9a70bdc5f682801be347a57eb1c591deb8cab0df","chunker_version":"chunker-v5-output-budget-editunit+u15.0.0","estimator_version":"estimator-v0+u15.0.0","max_tokens_policy":"maxtok-v1-double-per-attempt","classifier_version":"classify-v1-refusal+echo015+loop+u15.0.0","pipeline_core":"C1","max_output_ratio":2,"min_max_tokens":512,"context_assembly":{"glossary_injection":"selective","glossary_token_budget":800,"cache_ttl":""},"segmentation":{"draft_budget_out":1797,"edit_ceiling_out":3200,"fertility_cjk":1.1978,"fertility_other":0.3852},"render_format_version":"renderfmt-v2-editor-src2dst+dc3-gender","memory_version":"bd451833d4ebb8a0ca07caf8deda76f027e1f42b78654987a22d215acf955eb7","postcheck_gate":false,"coverage":{"enabled":false},"style_check_version":"cheapgate-v3-dc-checkers+u15.0.0","sanitizer":{"enabled":true,"version":"sanitizer-v6"},"stages":[{"name":"draft","role":"translator","model":"fake-model","prompt_version":"v-golden","prompt_sha256":"26d0245a56d180eafd200ebe5311aab2488142c4a98dcddee21ea457814aa21a","temperature":0.3,"reasoning":"off","capability":{"Budget":"max_tokens","Temp":"send","TempValue":0,"Reasoning":{"Control":"none","OffEffort":"","OffExtraBody":null,"OnExtraBody":null},"MinMaxTokens":4000},"escalate_to":"fake-fallback","escalate_capability":{"Budget":"max_tokens","Temp":"send","TempValue":0,"Reasoning":{"Control":"none","OffEffort":"","OffExtraBody":null,"OnExtraBody":null},"MinMaxTokens":6000}}]} +snapshot_edit: 66a990322f96d7f8f6d4783c63dbee75833b532e31d6a95d2de81394880d0a41 +snapshot_edit_payload: {"brief_hash":"c2021b1b5e6c20f6084e425b9a70bdc5f682801be347a57eb1c591deb8cab0df","chunker_version":"chunker-v5-output-budget-editunit+u15.0.0","estimator_version":"estimator-v0+u15.0.0","max_tokens_policy":"maxtok-v1-double-per-attempt","classifier_version":"classify-v1-refusal+echo015+loop+u15.0.0","pipeline_core":"C1","max_output_ratio":2,"min_max_tokens":512,"context_assembly":{"glossary_injection":"selective","glossary_token_budget":800,"cache_ttl":""},"segmentation":{"draft_budget_out":1797,"edit_ceiling_out":3200,"fertility_cjk":1.1978,"fertility_other":0.3852},"render_format_version":"renderfmt-v2-editor-src2dst+dc3-gender","memory_version":"0a48d6ed9409de09f514af91aebdd622637621c9dc9464e8898c238791033d91","postcheck_gate":false,"coverage":{"enabled":false},"style_check_version":"cheapgate-v3-dc-checkers+u15.0.0","sanitizer":{"enabled":true,"version":"sanitizer-v6"},"stages":[{"name":"edit","role":"editor","model":"fake-model","prompt_version":"v-golden","prompt_sha256":"6a8e7f71139d6bc09a95159d1cb895e05b56d2f2f7417c5e20a7073c517c79f6","temperature":0.4,"reasoning":"off","capability":{"Budget":"max_tokens","Temp":"send","TempValue":0,"Reasoning":{"Control":"none","OffEffort":"","OffExtraBody":null,"OnExtraBody":null},"MinMaxTokens":4000}}]} brief_hash: c2021b1b5e6c20f6084e425b9a70bdc5f682801be347a57eb1c591deb8cab0df -memory_version: 740aefe880a34396172775751d41d0e9ef136d053f3200a4f94391fa72aee7e4 -base_memory_version: 173e87fc011e832c108711097eeffc05db95ab56eafe2ef913b43829505cdfdd +memory_version: 0a48d6ed9409de09f514af91aebdd622637621c9dc9464e8898c238791033d91 +base_memory_version: bd451833d4ebb8a0ca07caf8deda76f027e1f42b78654987a22d215acf955eb7 -- book result -- chunks=8 flagged=4 exit=2 total_usd=0 chunk ch1/0 disposition=ok flag="" final_text="ОТРЕДАКТИРОВАННЫЙ ПЕРЕВОД 22c30f20dea6. Судзуки шёл по коридорам Академии магии." cost=0 @@ -178,58 +178,58 @@ chunk ch8/0 disposition=flagged flag="sanitizer_stripped" final_text="Судзу stage=edit role=editor model=fake-model resume=true disp=flagged flag="sanitizer_stripped" attempts=1 escalated=false esc_model="" finish="sanitized_export" cum_usd=0.00182 detail="CJK-утечка в ru-выходе: 特产" stage_text="" recovered="Судзуки нашёл древний камень на каменном алтаре." -- chunk_status -- -ch1/0 draft snap_match=true content_hash=331c6f32c7f3ed15447e2639a2621d24a180e561a2aee92115324f492db5de3e disp=ok flag="" attempts=1 final_hash=96f55c8caeec48506a73141758fe8e819c9da32c9f6d969e02b83057cc2a7af3 cost=0.00182 escalated=false esc_model="" detail="" -ch1/0 edit snap_match=true content_hash=8a4e763f79c94ad20a1bac0231a8c1376ccd4f5a71c53cdfcb4de146c0b48b42 disp=ok flag="" attempts=1 final_hash=dd9978846ec37ddde3e4db428582661271fd51e8cccbce1374fd2200800e9614 cost=0.00182 escalated=false esc_model="" detail="" -ch1/1 draft snap_match=true content_hash=12fe3ccdf062f43e011363a33ff85c9b6842a8f23d790991fe9afab51a8becc8 disp=ok flag="" attempts=1 final_hash=c9b9807b0279cb186cfae7e1255b2ea6e608b994b3429df9e7cb48f40c21b520 cost=0.00182 escalated=false esc_model="" detail="" -ch2/0 draft snap_match=true content_hash=2693325bc747687acf090bc263fe8c17783726b949c6d8a2d072da3401be6827 disp=ok flag="" attempts=1 final_hash=999ab8cd87c3505efaade5cc30b7f2d38a4e3fc6183250f34906e5c2db84f078 cost=0.00728 escalated=true esc_model="fake-fallback" detail="" -ch2/0 edit snap_match=true content_hash=5a5397d41bcfa9326b5fe67c70aca8d9ef14ef89af2dbe1a758e25d364226fc5 disp=ok flag="" attempts=1 final_hash=894afeaecfc7bef655d937d01a1c69b920c9500d10e4de9d1095f4391a55adac cost=0.00182 escalated=false esc_model="" detail="" +ch1/0 draft snap_match=true content_hash=331c6f32c7f3ed15447e2639a2621d24a180e561a2aee92115324f492db5de3e disp=ok flag="" attempts=1 final_hash=365abc01adb22d047a3865c79231c2669f7db960fd07069d46547d562cfc537b cost=0.00182 escalated=false esc_model="" detail="" +ch1/0 edit snap_match=true content_hash=8a4e763f79c94ad20a1bac0231a8c1376ccd4f5a71c53cdfcb4de146c0b48b42 disp=ok flag="" attempts=1 final_hash=42e10469327dcf7d8409cda457a7eff626795a709ff0b37f3de260d7ca11986a cost=0.00182 escalated=false esc_model="" detail="" +ch1/1 draft snap_match=true content_hash=12fe3ccdf062f43e011363a33ff85c9b6842a8f23d790991fe9afab51a8becc8 disp=ok flag="" attempts=1 final_hash=6f883d099cd0924eab5db1ef3eb730aa50429e719ffeea95df93e33042e9fcc0 cost=0.00182 escalated=false esc_model="" detail="" +ch2/0 draft snap_match=true content_hash=2693325bc747687acf090bc263fe8c17783726b949c6d8a2d072da3401be6827 disp=ok flag="" attempts=1 final_hash=f7a858a11f0890da139789d27bea23b1c33c83ef989f2d07dc51d4b238d5e16e cost=0.00728 escalated=true esc_model="fake-fallback" detail="" +ch2/0 edit snap_match=true content_hash=5a5397d41bcfa9326b5fe67c70aca8d9ef14ef89af2dbe1a758e25d364226fc5 disp=ok flag="" attempts=1 final_hash=ff45d79e2d08b57328491e1ac5e0b6b67ba883346c44806b7a63e7adddcf65bf cost=0.00182 escalated=false esc_model="" detail="" ch3/0 draft snap_match=true content_hash=733fc6a89215efe112cfadd1d1fb361bb8a7ab1013994248a49da0dbd137548e disp=flagged flag="hard_refusal" attempts=1 final_hash= cost=0.00728 escalated=true esc_model="fake-fallback" detail="provider finish_reason=refusal" ch3/0 edit snap_match=true content_hash= disp=skipped flag="hard_refusal" attempts=0 final_hash= cost=0 escalated=false esc_model="" detail="skipped: a member draft chunk of this edit unit was flagged (hard_refusal)" -ch4/0 draft snap_match=true content_hash=7aabdcb89dea365c95363b2662144ca1a10b307d377f47a8e7cc19b5b872d287 disp=ok flag="" attempts=1 final_hash=ac14e02f697c4fc4963946365a118afb5e8ba974c0d3c6781aac17d403dec5fd cost=0.00182 escalated=false esc_model="" detail="" -ch4/0 edit snap_match=true content_hash=2854dfe1648b0cf0cbb683cb7ce94c5eedc9223b2ce20c99dc008c7add6317ef disp=ok flag="" attempts=1 final_hash=5db400bde72cc0e7991f5860a524a73db4b511c683ef918cad505ffba3a6e314 cost=0.00182 escalated=false esc_model="" detail="" -ch5/0 draft snap_match=true content_hash=d8aba42cabb41853a81875ebc725bb516dc2412e29de3a03bc1a68e90f8cf157 disp=ok flag="" attempts=1 final_hash=7a7a6a7c55f5dcd00a80d04e8da5b4bc0f4b9bca8a33baa24397b7dfa9424e20 cost=0.00182 escalated=false esc_model="" detail="" -ch5/0 edit snap_match=true content_hash=82b05e4c3f5ca8432d7eaa7d7d63c4a7766f47a08398953c9a9bc3dc8437039b disp=ok flag="" attempts=1 final_hash=3fdb393a5e9bcba9869c7e0125d89e07f8bf64bb33c0098d999a9dcf5f5285ae cost=0.00182 escalated=false esc_model="" detail="" -ch6/0 draft snap_match=true content_hash=9375b05780724cc354eba15a595da9a5f964219c0887042484256eed11fb566f disp=ok flag="" attempts=1 final_hash=a96545d035e16b72a6abce2274f2ea4664399da63b9aaf88e5dfb3a96af20e2d cost=0.00182 escalated=false esc_model="" detail="" +ch4/0 draft snap_match=true content_hash=7aabdcb89dea365c95363b2662144ca1a10b307d377f47a8e7cc19b5b872d287 disp=ok flag="" attempts=1 final_hash=6fb9e932865b46557a1f5e4a0852a6fe5b2ed728e54d5704801840f1dbe89ccb cost=0.00182 escalated=false esc_model="" detail="" +ch4/0 edit snap_match=true content_hash=2854dfe1648b0cf0cbb683cb7ce94c5eedc9223b2ce20c99dc008c7add6317ef disp=ok flag="" attempts=1 final_hash=5b1f425d363f501da5832d570ebeb036a8d780378abf3e92cdded87c247b9b6e cost=0.00182 escalated=false esc_model="" detail="" +ch5/0 draft snap_match=true content_hash=d8aba42cabb41853a81875ebc725bb516dc2412e29de3a03bc1a68e90f8cf157 disp=ok flag="" attempts=1 final_hash=03ed6d252620e8763a0afc2d535e1b6e1cf97ca1145a66aa08ea199ef7102c04 cost=0.00182 escalated=false esc_model="" detail="" +ch5/0 edit snap_match=true content_hash=82b05e4c3f5ca8432d7eaa7d7d63c4a7766f47a08398953c9a9bc3dc8437039b disp=ok flag="" attempts=1 final_hash=3367114609df41762ff6f64b57de90f85b6f1992987ebf875bc64e028a6a0a4b cost=0.00182 escalated=false esc_model="" detail="" +ch6/0 draft snap_match=true content_hash=9375b05780724cc354eba15a595da9a5f964219c0887042484256eed11fb566f disp=ok flag="" attempts=1 final_hash=1efa0b7620e14fa8cdff89a198efe113e2196a4a48717b1dfacfc9bc878d2ae8 cost=0.00182 escalated=false esc_model="" detail="" ch6/0 edit snap_match=true content_hash=592353321cc8000e792e0897446e4dd4f88a0280b9368b62cbd5c82b0c433193 disp=flagged flag="sanitizer_defect" attempts=1 final_hash= cost=0.00182 escalated=false esc_model="" detail="ведущая служебная преамбула: Вот перевод фрагмента:" -ch7/0 draft snap_match=true content_hash=d90c335cba3d1547a22bb3f656443cb432f84c12a577d43509cfe472cf86a2ac disp=ok flag="" attempts=1 final_hash=9af233395ddde718f61b1bbb4709095a43ea3c4e146ca10990e821607b92bca5 cost=0.00182 escalated=false esc_model="" detail="" -ch7/0 edit snap_match=true content_hash=e12521b6eb6dbc2cd35758d664d4d31894c528f092083b5cfe0e79c51be2663e disp=flagged flag="sanitizer_stripped" attempts=1 final_hash=tm-sanitized-v1:6f0567f25e056974a065d6ea5aedc9b204eeaa7ae7648bd7f3bf34366348355e cost=0.00182 escalated=false esc_model="" detail="markdown-заголовок в выходе: ### Глава 7" -ch8/0 draft snap_match=true content_hash=fbd27af11da2275cd1d47a0224970a5f8d073c6164f614efb6522ea96fa1e021 disp=ok flag="" attempts=1 final_hash=f5da217f1c2a26da0f7b7d946dada785e03cb30dc13600272cf74b0c54704451 cost=0.00182 escalated=false esc_model="" detail="" -ch8/0 edit snap_match=true content_hash=0c6030a943cb341390509e2ebed4bbfd52af328d36916ce2e341f43665cf9183 disp=flagged flag="sanitizer_stripped" attempts=1 final_hash=tm-sanitized-v1:e0ea15352185a07b28f7b8b2ca50567d1b8044c5d8ad54ad6fd9d3e45f761c51 cost=0.00182 escalated=false esc_model="" detail="CJK-утечка в ru-выходе: 特产" +ch7/0 draft snap_match=true content_hash=d90c335cba3d1547a22bb3f656443cb432f84c12a577d43509cfe472cf86a2ac disp=ok flag="" attempts=1 final_hash=3a1050fe790ac5eb004c8d3e80a05cc6ff5e6b46c5947f6a57a7145985591dfb cost=0.00182 escalated=false esc_model="" detail="" +ch7/0 edit snap_match=true content_hash=e12521b6eb6dbc2cd35758d664d4d31894c528f092083b5cfe0e79c51be2663e disp=flagged flag="sanitizer_stripped" attempts=1 final_hash=tm-sanitized-v1:e9a837dbcd8d6717d27413735debe11bc116df2f65655d16f6fcbf51cbf59e66 cost=0.00182 escalated=false esc_model="" detail="markdown-заголовок в выходе: ### Глава 7" +ch8/0 draft snap_match=true content_hash=fbd27af11da2275cd1d47a0224970a5f8d073c6164f614efb6522ea96fa1e021 disp=ok flag="" attempts=1 final_hash=e1e065fc34385f41fd51a4b0db42d5f391532775ef68ece015f261a33d6b68da cost=0.00182 escalated=false esc_model="" detail="" +ch8/0 edit snap_match=true content_hash=0c6030a943cb341390509e2ebed4bbfd52af328d36916ce2e341f43665cf9183 disp=flagged flag="sanitizer_stripped" attempts=1 final_hash=tm-sanitized-v1:08f107da1257ff083f73e9af1e8d3b5b90e699ad6343faa80bebe891744c7adf cost=0.00182 escalated=false esc_model="" detail="CJK-утечка в ru-выходе: 特产" -- request_log (insertion order) -- -ch1/0 draft role=translator req=fake-model actual=fake-model hash=96f55c8caeec48506a73141758fe8e819c9da32c9f6d969e02b83057cc2a7af3 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch1/1 draft role=translator req=fake-model actual=fake-model hash=c9b9807b0279cb186cfae7e1255b2ea6e608b994b3429df9e7cb48f40c21b520 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch2/0 draft role=translator req=fake-model actual=fake-model hash=2e498c5ae088fcd6701b94c2f2ad7761033b6f663a26a48554ae12ca2c2f2630 tm_hit=0 ok=0 finish="stop" degraded="cjk_artifact" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch2/0 draft role=translator req=fake-fallback actual=fake-fallback hash=999ab8cd87c3505efaade5cc30b7f2d38a4e3fc6183250f34906e5c2db84f078 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.0054600000000000004 tokens=1000/200/0/500/0 err="" -ch3/0 draft role=translator req=fake-model actual=fake-model hash=6b744aad02e6e2d255353035a3943a278bc407cf2b1f2abaac9503c0d21239da tm_hit=0 ok=0 finish="refusal" degraded="hard_refusal" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch3/0 draft role=translator req=fake-fallback actual=fake-fallback hash=c42d577ea9c432be30ea7716f15b779320113de3d1269559988fde390fe1589d tm_hit=0 ok=0 finish="refusal" degraded="hard_refusal" cost=0.0054600000000000004 tokens=1000/200/0/500/0 err="" -ch4/0 draft role=translator req=fake-model actual=fake-model hash=ac14e02f697c4fc4963946365a118afb5e8ba974c0d3c6781aac17d403dec5fd tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch5/0 draft role=translator req=fake-model actual=fake-model hash=7a7a6a7c55f5dcd00a80d04e8da5b4bc0f4b9bca8a33baa24397b7dfa9424e20 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch6/0 draft role=translator req=fake-model actual=fake-model hash=a96545d035e16b72a6abce2274f2ea4664399da63b9aaf88e5dfb3a96af20e2d tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch7/0 draft role=translator req=fake-model actual=fake-model hash=9af233395ddde718f61b1bbb4709095a43ea3c4e146ca10990e821607b92bca5 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch8/0 draft role=translator req=fake-model actual=fake-model hash=f5da217f1c2a26da0f7b7d946dada785e03cb30dc13600272cf74b0c54704451 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch1/0 edit role=editor req=fake-model actual=fake-model hash=dd9978846ec37ddde3e4db428582661271fd51e8cccbce1374fd2200800e9614 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch2/0 edit role=editor req=fake-model actual=fake-model hash=894afeaecfc7bef655d937d01a1c69b920c9500d10e4de9d1095f4391a55adac tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch4/0 edit role=editor req=fake-model actual=fake-model hash=5db400bde72cc0e7991f5860a524a73db4b511c683ef918cad505ffba3a6e314 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch5/0 edit role=editor req=fake-model actual=fake-model hash=3fdb393a5e9bcba9869c7e0125d89e07f8bf64bb33c0098d999a9dcf5f5285ae tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch6/0 edit role=editor req=fake-model actual=fake-model hash=8db26b560abdf5e538830d6e60d6552225140163668ee698936990ceba379189 tm_hit=0 ok=0 finish="stop" degraded="sanitizer_defect" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch7/0 edit role=editor req=fake-model actual=fake-model hash=9ba86bea5f00ed45d089fea42c4353ed4321ade3980514765e26cf891aedd319 tm_hit=0 ok=0 finish="stop" degraded="sanitizer_stripped" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch8/0 edit role=editor req=fake-model actual=fake-model hash=943591a7303b83478ab8f24af256c4e663b7b01f9fed670a704ae10a330798f8 tm_hit=0 ok=0 finish="stop" degraded="sanitizer_stripped" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch1/0 draft role=translator req=fake-model actual=fake-model hash=96f55c8caeec48506a73141758fe8e819c9da32c9f6d969e02b83057cc2a7af3 tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" -ch1/1 draft role=translator req=fake-model actual=fake-model hash=c9b9807b0279cb186cfae7e1255b2ea6e608b994b3429df9e7cb48f40c21b520 tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" -ch2/0 draft role=translator req=fake-model actual=fake-fallback hash=999ab8cd87c3505efaade5cc30b7f2d38a4e3fc6183250f34906e5c2db84f078 tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" +ch1/0 draft role=translator req=fake-model actual=fake-model hash=365abc01adb22d047a3865c79231c2669f7db960fd07069d46547d562cfc537b tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch1/1 draft role=translator req=fake-model actual=fake-model hash=6f883d099cd0924eab5db1ef3eb730aa50429e719ffeea95df93e33042e9fcc0 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch2/0 draft role=translator req=fake-model actual=fake-model hash=0932d7aee2cd8fe944a4f5fcc4bf5b1828d8514ecc46cde7b5a3bd9983864dd2 tm_hit=0 ok=0 finish="stop" degraded="cjk_artifact" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch2/0 draft role=translator req=fake-fallback actual=fake-fallback hash=f7a858a11f0890da139789d27bea23b1c33c83ef989f2d07dc51d4b238d5e16e tm_hit=0 ok=1 finish="stop" degraded="" cost=0.0054600000000000004 tokens=1000/200/0/500/0 err="" +ch3/0 draft role=translator req=fake-model actual=fake-model hash=7be9635f28275d919b1ff3ff6aab9f94039eff250163bef3d6d3460489984b7e tm_hit=0 ok=0 finish="refusal" degraded="hard_refusal" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch3/0 draft role=translator req=fake-fallback actual=fake-fallback hash=ef672cdf6862522d15d93c32faf95d7eebc2c6ff4c24de6b0679a256900349ac tm_hit=0 ok=0 finish="refusal" degraded="hard_refusal" cost=0.0054600000000000004 tokens=1000/200/0/500/0 err="" +ch4/0 draft role=translator req=fake-model actual=fake-model hash=6fb9e932865b46557a1f5e4a0852a6fe5b2ed728e54d5704801840f1dbe89ccb tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch5/0 draft role=translator req=fake-model actual=fake-model hash=03ed6d252620e8763a0afc2d535e1b6e1cf97ca1145a66aa08ea199ef7102c04 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch6/0 draft role=translator req=fake-model actual=fake-model hash=1efa0b7620e14fa8cdff89a198efe113e2196a4a48717b1dfacfc9bc878d2ae8 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch7/0 draft role=translator req=fake-model actual=fake-model hash=3a1050fe790ac5eb004c8d3e80a05cc6ff5e6b46c5947f6a57a7145985591dfb tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch8/0 draft role=translator req=fake-model actual=fake-model hash=e1e065fc34385f41fd51a4b0db42d5f391532775ef68ece015f261a33d6b68da tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch1/0 edit role=editor req=fake-model actual=fake-model hash=42e10469327dcf7d8409cda457a7eff626795a709ff0b37f3de260d7ca11986a tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch2/0 edit role=editor req=fake-model actual=fake-model hash=ff45d79e2d08b57328491e1ac5e0b6b67ba883346c44806b7a63e7adddcf65bf tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch4/0 edit role=editor req=fake-model actual=fake-model hash=5b1f425d363f501da5832d570ebeb036a8d780378abf3e92cdded87c247b9b6e tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch5/0 edit role=editor req=fake-model actual=fake-model hash=3367114609df41762ff6f64b57de90f85b6f1992987ebf875bc64e028a6a0a4b tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch6/0 edit role=editor req=fake-model actual=fake-model hash=db25b5d375286a9fa18bc3b7b9da14c8bd95a5209b8b2e9099889413062504d1 tm_hit=0 ok=0 finish="stop" degraded="sanitizer_defect" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch7/0 edit role=editor req=fake-model actual=fake-model hash=54ab4a1ffb853422a3b079b71b3c1c7ba11eb434c3d9f1df3c81aec2f007e0d8 tm_hit=0 ok=0 finish="stop" degraded="sanitizer_stripped" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch8/0 edit role=editor req=fake-model actual=fake-model hash=c047ff047484181ff697d1e525c3d12f5b76ab5f9451cc5f309be3465559cf5e tm_hit=0 ok=0 finish="stop" degraded="sanitizer_stripped" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch1/0 draft role=translator req=fake-model actual=fake-model hash=365abc01adb22d047a3865c79231c2669f7db960fd07069d46547d562cfc537b tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" +ch1/1 draft role=translator req=fake-model actual=fake-model hash=6f883d099cd0924eab5db1ef3eb730aa50429e719ffeea95df93e33042e9fcc0 tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" +ch2/0 draft role=translator req=fake-model actual=fake-fallback hash=f7a858a11f0890da139789d27bea23b1c33c83ef989f2d07dc51d4b238d5e16e tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" ch3/0 draft role=translator req=fake-model actual=fake-model hash= tm_hit=1 ok=0 finish="" degraded="hard_refusal" cost=0 tokens=0/0/0/0/0 err="" -ch4/0 draft role=translator req=fake-model actual=fake-model hash=ac14e02f697c4fc4963946365a118afb5e8ba974c0d3c6781aac17d403dec5fd tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" -ch5/0 draft role=translator req=fake-model actual=fake-model hash=7a7a6a7c55f5dcd00a80d04e8da5b4bc0f4b9bca8a33baa24397b7dfa9424e20 tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" -ch6/0 draft role=translator req=fake-model actual=fake-model hash=a96545d035e16b72a6abce2274f2ea4664399da63b9aaf88e5dfb3a96af20e2d tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" -ch7/0 draft role=translator req=fake-model actual=fake-model hash=9af233395ddde718f61b1bbb4709095a43ea3c4e146ca10990e821607b92bca5 tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" -ch8/0 draft role=translator req=fake-model actual=fake-model hash=f5da217f1c2a26da0f7b7d946dada785e03cb30dc13600272cf74b0c54704451 tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" -ch1/0 edit role=editor req=fake-model actual=fake-model hash=dd9978846ec37ddde3e4db428582661271fd51e8cccbce1374fd2200800e9614 tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" -ch2/0 edit role=editor req=fake-model actual=fake-model hash=894afeaecfc7bef655d937d01a1c69b920c9500d10e4de9d1095f4391a55adac tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" -ch4/0 edit role=editor req=fake-model actual=fake-model hash=5db400bde72cc0e7991f5860a524a73db4b511c683ef918cad505ffba3a6e314 tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" -ch5/0 edit role=editor req=fake-model actual=fake-model hash=3fdb393a5e9bcba9869c7e0125d89e07f8bf64bb33c0098d999a9dcf5f5285ae tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" +ch4/0 draft role=translator req=fake-model actual=fake-model hash=6fb9e932865b46557a1f5e4a0852a6fe5b2ed728e54d5704801840f1dbe89ccb tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" +ch5/0 draft role=translator req=fake-model actual=fake-model hash=03ed6d252620e8763a0afc2d535e1b6e1cf97ca1145a66aa08ea199ef7102c04 tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" +ch6/0 draft role=translator req=fake-model actual=fake-model hash=1efa0b7620e14fa8cdff89a198efe113e2196a4a48717b1dfacfc9bc878d2ae8 tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" +ch7/0 draft role=translator req=fake-model actual=fake-model hash=3a1050fe790ac5eb004c8d3e80a05cc6ff5e6b46c5947f6a57a7145985591dfb tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" +ch8/0 draft role=translator req=fake-model actual=fake-model hash=e1e065fc34385f41fd51a4b0db42d5f391532775ef68ece015f261a33d6b68da tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" +ch1/0 edit role=editor req=fake-model actual=fake-model hash=42e10469327dcf7d8409cda457a7eff626795a709ff0b37f3de260d7ca11986a tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" +ch2/0 edit role=editor req=fake-model actual=fake-model hash=ff45d79e2d08b57328491e1ac5e0b6b67ba883346c44806b7a63e7adddcf65bf tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" +ch4/0 edit role=editor req=fake-model actual=fake-model hash=5b1f425d363f501da5832d570ebeb036a8d780378abf3e92cdded87c247b9b6e tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" +ch5/0 edit role=editor req=fake-model actual=fake-model hash=3367114609df41762ff6f64b57de90f85b6f1992987ebf875bc64e028a6a0a4b tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" ch6/0 edit role=editor req=fake-model actual=fake-model hash= tm_hit=1 ok=0 finish="" degraded="sanitizer_defect" cost=0 tokens=0/0/0/0/0 err="" -ch7/0 edit role=editor req=fake-model actual=fake-model hash=tm-sanitized-v1:6f0567f25e056974a065d6ea5aedc9b204eeaa7ae7648bd7f3bf34366348355e tm_hit=1 ok=0 finish="sanitized_export" degraded="sanitizer_stripped" cost=0 tokens=0/0/0/0/0 err="" -ch8/0 edit role=editor req=fake-model actual=fake-model hash=tm-sanitized-v1:e0ea15352185a07b28f7b8b2ca50567d1b8044c5d8ad54ad6fd9d3e45f761c51 tm_hit=1 ok=0 finish="sanitized_export" degraded="sanitizer_stripped" cost=0 tokens=0/0/0/0/0 err="" +ch7/0 edit role=editor req=fake-model actual=fake-model hash=tm-sanitized-v1:e9a837dbcd8d6717d27413735debe11bc116df2f65655d16f6fcbf51cbf59e66 tm_hit=1 ok=0 finish="sanitized_export" degraded="sanitizer_stripped" cost=0 tokens=0/0/0/0/0 err="" +ch8/0 edit role=editor req=fake-model actual=fake-model hash=tm-sanitized-v1:08f107da1257ff083f73e9af1e8d3b5b90e699ad6343faa80bebe891744c7adf tm_hit=1 ok=0 finish="sanitized_export" degraded="sanitizer_stripped" cost=0 tokens=0/0/0/0/0 err="" -- retrieval_state -- ch1/0 snap_match=true exact=3 sticky=0 ambiguous=0 spoiler=1 evicted=0 postcheck_miss=1 style_flags=0 trust_gated=0 injected_ids=["紋章\u001f\u001f0\u001f0","鈴木\u001f\u001f0\u001f0","魔法学院\u001f\u001f0\u001f0"] postcheck_detail=[{"src":"紋章","dst":"герб","disp":"confirmed"}] style_detail= trust_gate_detail= diff --git a/backend/internal/pipeline/waverun.go b/backend/internal/pipeline/waverun.go index a0334eb9..7488e95c 100644 --- a/backend/internal/pipeline/waverun.go +++ b/backend/internal/pipeline/waverun.go @@ -69,12 +69,15 @@ func (r *Runner) outputUnits(chunks []Chunk) []editUnit { return out } -// WaveSignatureStop is the typed sentinel the CLI surfaces when bank-mining proposes a non-empty seed-delta: -// the run completes the draft wave, writes the owner signature map, and STOPS before the edit wave (plan §1(в) default — an explicit -// stop boundary, not a mid-run append). The owner reviews the signature map, curates the mined-delta file -// (promoting terms to approved with a dst), and re-runs: the draft wave resumes at $0, the bank-mining stop re-mines (the now-seeded -// terms are excluded → the delta empties → auto-continue), and the edit wave runs under edit-wave snapshot. NOT an infra -// failure — a deliberate human-in-the-loop pause, carried up like CompletedWithFlags. +// WaveSignatureStop is the typed sentinel the driver returns when bank-mining proposes a non-empty +// seed-delta: the run completes the draft wave, writes the owner signature map, and STOPS before the edit +// wave (plan §1(в) default — an explicit stop boundary, not a mid-run append). The owner reviews the +// signature map, then either promotes a term into the mined-delta file (approved + dst) or declines it in +// the mined-rejects file, and re-runs: the draft wave resumes at $0, the bank-mining stop re-mines (the +// now-seeded AND rejected terms are excluded → the delta empties → auto-continue), and the edit wave runs +// under the edit-wave snapshot. NOT an infra failure — a deliberate human-in-the-loop pause. The CLI maps +// it (errors.As) to a DISTINCT exit code (3, unlike CompletedWithFlags's 2 or a crash's 1) and renders the +// terms + signature-map path to the operator (main.go/renderSignatureStop, R1-FL-A). type WaveSignatureStop struct { Terms int SignaturePath string diff --git a/backend/internal/pipeline/waverun_test.go b/backend/internal/pipeline/waverun_test.go index 2010347c..2805b561 100644 --- a/backend/internal/pipeline/waverun_test.go +++ b/backend/internal/pipeline/waverun_test.go @@ -99,8 +99,8 @@ func TestWaveMultiChunkEditUnit(t *testing.T) { if err != nil { t.Fatal(err) } - if exp.TotalChunks != 1 || exp.PendingChunks != 0 || len(exp.Chunks) != 1 { - t.Fatalf("export must be 1 unit / 0 pending, got total=%d pending=%d rows=%d", exp.TotalChunks, exp.PendingChunks, len(exp.Chunks)) + if exp.TotalUnits != 1 || exp.PendingUnits != 0 || len(exp.Chunks) != 1 { + t.Fatalf("export must be 1 unit / 0 pending, got total=%d pending=%d rows=%d", exp.TotalUnits, exp.PendingUnits, len(exp.Chunks)) } if exp.Chunks[0].FinalText != "ОТРЕДАКТИРОВАННЫЙ ПЕРЕВОД" { t.Fatalf("export unit text mismatch: %q", exp.Chunks[0].FinalText) @@ -115,8 +115,8 @@ func TestWaveMultiChunkEditUnit(t *testing.T) { if err != nil { t.Fatal(err) } - if st.TotalChunks != 1 || st.Done != 1 { - t.Fatalf("status must be 1 unit done, got total=%d done=%d", st.TotalChunks, st.Done) + if st.TotalUnits != 1 || st.Done != 1 { + t.Fatalf("status must be 1 unit done, got total=%d done=%d", st.TotalUnits, st.Done) } r2.Close() @@ -156,6 +156,11 @@ func TestRunWaveSurfacesParentCancellation(t *testing.T) { // many chunks and asserts the money invariant survives concurrency: committed spend == the sum of the // settled checkpoints (the single-writer store serializes Reserve/Settle), and the per-unit result is // complete + deterministic. Run under `-race` this also proves the waves share only read-only state. +// +// It also exercises the REAL internal/llm HTTP adapter under concurrency (D39.17-errata nit): the 4 workers +// fire concurrent requests through the real transport at an httptest.Server (setupProjectOpts writes a +// models.yaml pointing at the live URL — not a fake in-process client), so a shared-client data race would +// trip -race here. stdlib http.Client is safe for concurrent use by design; this pins it end-to-end. func TestWaveParallelWorkersMoneyConserved(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, draftEdit) @@ -267,14 +272,19 @@ func TestWaveEditUnitFlaggedMemberDraft(t *testing.T) { if exp.Chunks[0].FinalText != "ОТРЕДАКТИРОВАННЫЙ ПЕРЕВОД" { t.Fatalf("export must ship the edited clean remainder for a dropped-member unit, got %q", exp.Chunks[0].FinalText) } - // The dropped member's echo must NOT vanish from the quality telemetry the owner reads for acceptance - // (c-lite makes the leader edit row DispOK; quality re-derives the drop like export/status). + // Echo split (D39.18): the dropped member's echo is a TRANSLATOR echo → it counts in echo_draft + // (read DIRECTLY off the draft row, no c-lite re-derivation). The editor recovered the unit over the + // clean remainder, so its own output did NOT echo → echo_edit is 0. This is the load-bearing case the + // split fixes: the draft echo stays visible while the delivered (edit) quality is honestly clean. q, err := r2.QualityReport() if err != nil { t.Fatal(err) } - if q.EchoChunks != 1 { - t.Fatalf("the dropped member's echo must still count in QualityReport (EchoRate not silently regressed), got EchoChunks=%d", q.EchoChunks) + if q.EchoDraftChunks != 1 || q.EchoDraftRate != 0.5 { // 1 of 2 draft chunks echoed + t.Fatalf("the dropped member's echo must count in echo_draft, got EchoDraftChunks=%d rate=%.3f (want 1 / 0.5)", q.EchoDraftChunks, q.EchoDraftRate) + } + if q.EchoEditUnits != 0 || q.EchoEditRate != 0 { + t.Fatalf("the editor recovered the clean remainder → echo_edit must be 0, got EchoEditUnits=%d rate=%.3f", q.EchoEditUnits, q.EchoEditRate) } } diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index aaa5157c..d883e956 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -9,6 +9,10 @@ > - **Ждём от владельца:** тачпойнты exp16 (карта подписи/пол · мини-голд алиасов · precision@30, `books/gu-zhenren/exp16/`, ~30–40 мин — для сид-дельты к пере-прогону) · развилки плана §10 (W1.5-UX · DC5-стих-политика · Edit-ceiling · и др.) · реплика ja→ru (тест общности §B5) · **ре-чек прайса DeepSeek у катовера слагов 24.07** (до него платных deepseek-прогонов нет) · чтение пере-прогона (после R1+resnapshot) · старые висящие: планка запуска (лучше-фана/гибрид/издательский) · билингв-якорь пилота (D25.9-Q1) · контаминация пилот-корпуса (D27.4) · publishable/waiver (D25.1) · FN-bound L3 (D25.4) · юр-пакет · провенанс 12-*-доков. > - Архивы хроники: `archive/PROGRESS-2026-07-04-10.md` (D31) · `archive/PROGRESS-2026-07-10-13.md` (D39.6-гигиена). Записи ниже — живая эра D39. +## Оркестратор №7 — пере-прогон-преп ПРИНЯТ и залендён (D39.19), бэкенд готов к пере-прогону, 19.07 + +Сессия исполнила все 6 фиксов препа + §8-верификацию + свой 4-линзовый адверсариал (clean bill). **Залендено:** R1-FL-A (CLI exit-3 для WaveSignatureStop) · R1-FL-B (reject-set `Book.MinedRejects` — livelock/W1.5-UX закрыт; nil no-op, парити цел) · R1-FL-C (x/text-версия в Ш-2, pin+drift-чек — единственная снапшот-двигающая) · echo-сплит (draft/edit rates, костыль снят) · read-model naming (chunk→unit, полигон не сломан) · ниты (мёртвый minedToCandidates снесён). **Приёмка прямая (локальные фиксы, сессия сама адверсариалила):** build/race зелёные сам, **независимый masked-golden = 0 после маскировки хешей** (version-only чисто, wire/вердикты байт-идентичны), пины все (exit-контракт, reject-suppress, x/text-drift, echo-сплит, парити EXACT), §8-манифест полон (+x/text). Док-правка: x/text-строка добавлена в план §8 (моя зона). **ПАК-11+преп завершены, бэкенд готов.** Промт препа отработан→архив. **Очередь: пере-прогон** (операционная: ОДИН resnapshot + langpack+contrast+подписанная карта+армы; риг D39.7; finish=stop) → чтение (планка 2 претензии). Гейт: ре-чек прайса DeepSeek 24.07. + ## Оркестратор №7 — gender-signoff закрыт + промт пере-прогон-препа выдан, 19.07 **Gender-signoff карты exp16 ЗАКРЫТ** (владелец «male ок», оркестратор проставил корректно, не бланк-штампом): **male** (name-персоны he-доминантно) 方源·古月·方正·人祖·花酒行者·古月赤练/赤城/漠北/漠尘/冻土 · **female** 沈嬷嬷 (嬷嬷=нянька, 0/2) + **沈翠** (KWIC разрешил ничью 2/2: 丫鬟/丫头=служанка, дочь 沈嬷嬷 — счётчики 他 были от окружающих мужчин) · **hidden** 白凝冰 · **n/a** все title/term/place + 困境 («Беды»=имя стаи). Флагов не осталось. Карта (`books/gu-zhenren/exp16/signature_map.md`, вне git) — dst-канон + пол закрыты, готова к сид-дельте. **Промт `BACKEND_RERUN_PREP_SESSION_PROMPT.md` ВЫДАН** — 6 целевых фиксов (R1-FL-A CLI-exit · R1-FL-B mining-reject-set [дизайн-дефолт: reject-list файлом] · R1-FL-C x/text-версия в Ш-2 · echo-сплит · read-model naming · лёгкие ниты) + верификация полноты §8-манифеста единого resnapshot; директива «не упарываться на миноры» вшита. Пере-прогон (ОДИН resnapshot fresh-translate + армы + подписанная карта) — операционная сессия ПОСЛЕ препа; гейт: ре-чек прайса DeepSeek 24.07. diff --git a/docs/README.md b/docs/README.md index c603a5db..676613bc 100644 --- a/docs/README.md +++ b/docs/README.md @@ -16,7 +16,7 @@ - `experiments/` — эмпирика «Полигона»: `00-provider-quirks` (читать перед любым вызовом провайдера), `01-token-calibration`, `02-refusal-benchmark`, `03-local-stand`, `04-editor-quality`, `06-local-extraction`, `07-coverage-precision`, `08-cost-model-v2` (актуальная денежная модель), `09-pilot-protocol` (пилот Ф2.5 + поправки D13), `10-explicit-benchmark` (18+ violence-рука канала B), `11-erotica-benchmark` (erotica по трём парам/регистрам — закрытие D14.4, D22). - `research/` — фактура исследований 04–05.07: `01–10` базовые, `11-gap-*` добор критиком, `12-*` режимы отказа/отзывы/таксономии (+ два внешних материала с провенанс-шапками), `13` валидация памяти, `14` адаптивная память, `15` голос и состояние (принят, D21), **`16` ридер-IDE (принят с ревью-шапкой, D29)**, **`17` внешняя критика GPT-5.6 (принят с ревью-шапкой, D25)** — у 16/17 читать шапку прежде тела. **`18` рычаги качества (два отчёта, D36)** · **`19` нарезка+когезия+контракт t/e (D39.1)** · **`20` банк-майнинг W1.5 (D39.6)** — у всех ревью-шапки. ⚠ Часть под superseded-баннерами (01/02/03/04/05/09 и gap-1/2/5) — **читай баннер прежде содержимого**. - `PROGRESS.md` — **журнал** (CURRENT-STATE сверху, ниже хронология; НЕ источник решений). -- **Активные хендофф-промты (пост-D39.18, 19.07):** [`BACKEND_RERUN_PREP_SESSION_PROMPT.md`](BACKEND_RERUN_PREP_SESSION_PROMPT.md) (**СТАРТОВЫЙ — пере-прогон-преп: 6 целевых фиксов [R1-FL-A/B/C · echo-сплит · read-model naming · ниты] + готовность §8-манифеста; перед пере-прогоном**) · [`ORCHESTRATOR_SESSION_PROMPT.md`](ORCHESTRATOR_SESSION_PROMPT.md) (хендофф №5 оркестратора) · [`POLYGON_PACKAGE4_SESSION_PROMPT.md`](POLYGON_PACKAGE4_SESSION_PROMPT.md) (residual пилот/18+/echo). **ПАК-11 ЗАВЕРШЁН.** Очередь: преп → пере-прогон (ОДИН resnapshot + армы + подписанная карта) → чтение. Закрытые — в `archive/prompts/` (свежие: пак-11/R1→D39.17, арх-cleanup→D39.16). +- **Активные хендофф-промты (пост-D39.19, 19.07):** [`ORCHESTRATOR_SESSION_PROMPT.md`](ORCHESTRATOR_SESSION_PROMPT.md) (хендофф №5 оркестратора) · [`POLYGON_PACKAGE4_SESSION_PROMPT.md`](POLYGON_PACKAGE4_SESSION_PROMPT.md) (residual пилот/18+/echo). **ПАК-11 + ПРЕП ЗАВЕРШЕНЫ, бэкенд готов к пере-прогону.** Следующий шаг = **пере-прогон** (операционная сессия: ОДИН `--resnapshot` fresh-translate + langpack+contrast+подписанная карта exp16 + армы редактора; промт пишет оркестратор). Гейт: ре-чек прайса DeepSeek 24.07. Закрытые — в `archive/prompts/` (свежие: преп→D39.19, пак-11/R1→D39.17, арх-cleanup→D39.16). - `archive/` — закрытые сессионные промты (только история, инструкции оттуда не исполнять). ## Статус (2026-07-19, пост-D39.16 — стройка пере-прогонного стека) diff --git a/docs/architecture/05-decisions-log.md b/docs/architecture/05-decisions-log.md index 64594531..8ac6a350 100644 --- a/docs/architecture/05-decisions-log.md +++ b/docs/architecture/05-decisions-log.md @@ -3,7 +3,7 @@ > **⟶ КАРТА АКТУАЛЬНОСТИ (ревизия D31, продлена до D38.2 [12.07]; исторические записи ниже НЕ переписываются — дисциплина D23.3).** Читая контракт целиком, держи под рукой, что чем перекрыто: > - **Полностью superseded:** **D1 (моно-редактор) → D17 → D30.1 (редактор БИЛИНГВ)** · D9 (ja-приёмка) → D18 (蛊真人 zh→ru; ja — второй прогон) · D17 → D30.1 · скобка D19.4 «ключ без data-sharing» и D20.3 «чистый ключ = блокер пилота» → **D27** (единый ключ С data-sharing, отключение перед продом) · exp04-дефолт «editor grok-4.3» (D3) → D30.1 (grok reasoning-off из редакторских ролей СНЯТ — no-op; кандидат glm-5-билингв; gemini — премиум-эскалация только за санитайзером D30.3). > - **Частично амендировано:** D3 (канал B → D14.1/D19.1 + оговорки D22.5/D22.6; апекс-слаг `-preview` — D22.4; draft под вопросом exp13 — D30.6) · D6 («off/minimal для draft/edit» эродирован: draft thinking-ON принудительно, editor-off снят D30.1; живы per-роль принцип и D6.2-буфер) · D10 (+D24.2 alias-слепое пятно, истинная консистентность ≈99.5%) · D11-числа → exp08 → **D30.4** (флип D1 = +15–25%, ~$0.85/ранобэ; «$1.5–2» = неподписанная опция Б) · D12 (+D24.3 флоры max_tokens; +D29.1в rollup — амендмент вердикт-правила) · D13.1-арм из решающего → ПОДТВЕРЖДАЮЩИЙ (D30.1); +D25.3 prefix-анализ судьи · D14.2 закрыт D19.1/D22.4; D14.4 закрыт D22.1 · D15.3 исполнен; спека D15.2: v2→v3→v3.1 (D22.2), реализация = текущий пакет (D30.9) · D24.4 +D28.1 (precision/recall-трейдофф; hard-gate требует пере-замера) · порядок D24.5 отложен D26.1 (этап B — после читаемых 25 глав). -> - **Живое ядро без изменений:** D2 (+новый гейт-класс D30.3), D4, D5, D7, D8, D10-механизм, D12-таксономия отказов, D16, D18, D19.1–19.2, D21 (Ф2-механизмы voice/address/reveal), D22.5–22.7, D23 (golden = инвариант №8), D24–D39.18 — действующая голова контракта (**D39.18 = blast-radius edit-единицы: c-lite (редактура чистого остатка вместо бланка всей единицы; вариант владельца (c)+fallback (b)) залендён `488c8d4`, no-flag путь байт-идентичен; + 4 read-модель-фикса пост-лендинг-ревью [quality-echo/status-first-flagged/export-pairs/detail], открытое решение владельцу = echo-метрика семантика; R1-FL-A/B/C deferred в пере-прогон-преп**; **D39.17-errata = runWave parent-cancellation баг [мой рубеж-2 пропустил, 2-я сессия поймала], фикс `5fc4860`**; **D39.17 = R1 драйвер-свитч ЗАЛЕНДЕН [волновой исполнитель черновик∥→банк-майнинг-стоп→редактура∥; редактор не пере-рендерит draft; Ш-1/Ш-2; golden-рерайт; read-модели per-unit]; полный рубеж-2 [5 линз вкл. синк-с-ресёрчем] = clean bill по несущим осям, 3 MINOR/NOTE-фикса (R1-FL-A CLI-exit-код · R1-FL-B mining-стоп-reject · R1-FL-C x/text-версия) в стенд-only mining-пути → пере-прогон-преп; ПАК-11 ЗАВЕРШЁН**; **D39.16 = арх-проход ЗАЛЕНДЕН: майнер-данные (百家姓/Палладий/частицы/суффиксы) вынесены в `internal/lang`+`configs/langpacks/` [граница компиль-enforced, байт-точно — парити EXACT, golden байт-идентичен]; FL-1/2/3+тест-путь; РАТИФИЦИРОВАНЫ 3 дивергенции: language.Tag ОТВЕРГНУТ [CLDR-инстабильность в вердиктах], DC-чекеры+снапшот-фолд DEFER [майнер offline/нейтральность]; общность структурно доказана; промт отработан→архив**; **D39.15 = директива владельца: изоляция языковых данных из `pipeline/` [движок общий · `internal/lang/`+`configs/langpacks/` данными-файлами · `x/text/language.Tag` вместо `isRuTarget` · снапшот-folded как промпты · алгоритмы не трогать]; архитектурный проход ПЕРЕД R1 [ре-секвенс]; поведенчески нейтрально zh→ru [golden байт-стабилен]; не минор-хант/не rewrite-ради-rewrite; промт `BACKEND_ARCH_CLEANUP_SESSION_PROMPT.md` 5-фазный [ресёрч→синтез→фикс-лист→исполнение→ревью]; ja→ru = тест общности §B5 позже**; **D39.14 = пак-11 continuation ПРИНЯТ и залендён [R2 майнер паритет-EXACT · R3 банкнота 12-точек · R4 DC-чекеры+export--pairs · R5 арм-конфиги · R1-аддитив; рубеж-2 D39.13-долга погашен: 3 MINOR/PARTIAL-находки [FL-1 bankTokenBudget-фолд · FL-2 banknote-телеметрия-резюм · FL-3 emission-top-N-кап], все латентны → R1-продолжение; R1 драйвер-свитч резидуал; clean bill по деньгам/wire/порту]**; **D39.13 = пак-11 Block A ПРИНЯТ и залендён [WS2 целиком (вкл. src→dst-блок редактора = закрытие D30.1) + WS1a/b/c-инфра + WS4-ядро; 2 девиации ратифицированы; резидуал R1–R5 в PACK11_REPORT; промт остаётся активным; рубеж-2 приёмки — долг]**; **D39.12 = план `architecture/11-implementation-plan.md` РАТИФИЦИРОВАН [три рубежа: ревью-2 F1–F11 → 7-агентный адверсариал → подтверждающий пас; пер-волновой снапшот + инвариант секвенирования = «переоплата ОДНА»; EditCeilingOut=3200; лемматизатор дефолт B; 4 гейтнутых арма]; бэкенд-пак ЕДИНОЙ сессией — `BACKEND_PLAN11_SESSION_PROMPT.md`**; **D39.11 = директива владельца: стройка ТОЛЬКО после дизайн-синтез сессии с верифицированными алгоритмами; хендофф №5**; **D39.10 = exp16: WHICH подтверждена [V-C recall 0.97, $0] / WHAT→банкнота+Палладий [canon-recovery <70%], 9B-споттер не нужен, Z1→облако, ре-аудит exp14b чист [errata не требуется]; исследовательская программа ЗАВЕРШЕНА → бэкенд-пак слоёв 1+4**; **D39.9 = Q4a: верность НЕ на translate-стадии [flash уже верен, pro ×3.67 не ратифицирован], смысл ломает glm-РЕДАКТОР [свап-арм приоритетен], DET-пол 0.261; exp14b-правила дефектны → $0-ре-аудит в exp16**; **D39.8 = слепое чтение: планка не пройдена никем, дефекты распределены [читатель подтвердил floor-null], битва качества → дефект-классы [时辰-юниты, числа, gender-enforce, стих, регистр]**; **D39.7 = exp15 залендён REV.2: «граница вредит» отозван [артефакт окна судьи], все когезия-эффекты под floor 0.126, «sequential нигде не лучше» LOO-stable [не ратифицированный зелёный свет], фертильность 1.20/0.39, $12.79/$15; гейт = слепое чтение владельца**; **D39.6 = research/20 банк-майнинг залендён [детектор-лестница V-A/B/C, алиас-ярусы, banknote-v1 с 12 гейт-точками, §B5 плагины P1–P6, §D-пре-рег]; полигон-стадия после exp15**; **D39.5 = пак-1.5+F1–F9 залендены [tmctl export с manifest/drift-гардами · fold-first санитайзер · bounded глосс · пример zh-ru]; экстракция полигона впредь через tmctl export**; **D39.4 = адверсариал-долг ПОГАШЕН [8 осей: 0 CRIT/1 HIGH/6 MED], пак-1.5 придержан под фикс-лист F1–F9 в §T4**; **D39.3 = пакет банк-майнинга W1.5 [диспозиция H15/V4-п4] ратифицирован, research/20-промт с HOLD до фриза exp15**; **D39.2 = трек-A пак-1 залендён `d3f6b34`: trust-gated suppressor [терм-дрейф закрыт в КОДЕ] + export-contract нормализация + pair-сеам + реестры + quality-report; адверсариал-долг за 529**; **D39.1 = research/19 залендён: конфликт exp14↔research/18 разрешён [эмиссия≠окно≠связность], Go-спека чанкера+когезии, волновая архитектура W0–W3 [вводная владельца: параллелизм], пре-рег эмпирики Q0–Q5; оркестратор-ревью снято владельцем**; **D39 = АРХ-РЕСЕТ: синк-аудит сломанного телефона [65 находок, 0 refuted, `08-sync-audit-ledger.md`] + целевая 7-слойная архитектура [`09-target-architecture.md`] + фазовый план [трек A build-now ∥ трек B ресёрч, пере-прогон после]; формализует фантомный «D35.7» как цель слоя 1; терм-дрейф код-корень жив [сид-воркэраунд]; редактор 4-мандата-full-regen = причина инверсий**; D35 = пивот качество-первым; D36 = приёмка research/18; D37 = приёмка exp14: претензия-1=промпт-рычаг; D38 = приёмка exp14b: «только gpt-5.4» ОПРОВЕРГНУТО [mistral/deepseek-pro чинят], фронтир в дефолт НЕ нужен, корень терм-дрейфа = статус-draft сида; **D38.1 = слепой h2h P1a↔F-disc залендён [ChatGPT «фронтир сильнее» / Claude «near-parity» расходятся, СХОДЯТСЯ: дефекты = инфра] → РАЗВОРОТ «мерить→строить», выдан BACKEND_INFRA_PACK; D38.2 = консолидация/док-гигиена: спент exp-скрипты в `eval/exp12|13|14|14b/`, статус-доки+.puml актуализированы; D38.3 = инфра-пак читаемости залендён (санитайзер-классы strip+export + число/omission-гард), верифицирован исполнением [build/vet/test зелёные, 3 span-дефекта запинены]; D38.4 = аудит полноты exp14/14b: чэнъюй-рычаг забыт near-term→дописан в дискурс-промпт; inversion-гард D37 §2г не строился (число/omission ≠ полярность)→editor-swap = защита от инверсий (приоритет↑); нарезка = re-run-арм; гард-тумблер вкл. при resnapshot; D38.5 = компоненты resnapshot залендены+верифицированы [reseed-сид eb409f2 · дискурс+чэнъюй editor v3 0ac5f7a с few_shot-тумблером] → следующий шаг единый resnapshot + пере-прогон**). +> - **Живое ядро без изменений:** D2 (+новый гейт-класс D30.3), D4, D5, D7, D8, D10-механизм, D12-таксономия отказов, D16, D18, D19.1–19.2, D21 (Ф2-механизмы voice/address/reveal), D22.5–22.7, D23 (golden = инвариант №8), D24–D39.19 — действующая голова контракта (**D39.19 = пере-прогон-преп ЗАЛЕНДЕН [6 фиксов: R1-FL-A CLI-exit-3 · R1-FL-B reject-set-livelock-закрыт · R1-FL-C x/text-в-Ш-2 · echo-сплит · read-model-naming · ниты]; golden version-only [masked=0]; парити EXACT; §8-манифест полон; gender-signoff закрыт; бэкенд готов к пере-прогону**; **D39.18 = blast-radius edit-единицы: c-lite (редактура чистого остатка вместо бланка всей единицы; вариант владельца (c)+fallback (b)) залендён `488c8d4`, no-flag путь байт-идентичен; + 4 read-модель-фикса пост-лендинг-ревью [quality-echo/status-first-flagged/export-pairs/detail], открытое решение владельцу = echo-метрика семантика; R1-FL-A/B/C deferred в пере-прогон-преп**; **D39.17-errata = runWave parent-cancellation баг [мой рубеж-2 пропустил, 2-я сессия поймала], фикс `5fc4860`**; **D39.17 = R1 драйвер-свитч ЗАЛЕНДЕН [волновой исполнитель черновик∥→банк-майнинг-стоп→редактура∥; редактор не пере-рендерит draft; Ш-1/Ш-2; golden-рерайт; read-модели per-unit]; полный рубеж-2 [5 линз вкл. синк-с-ресёрчем] = clean bill по несущим осям, 3 MINOR/NOTE-фикса (R1-FL-A CLI-exit-код · R1-FL-B mining-стоп-reject · R1-FL-C x/text-версия) в стенд-only mining-пути → пере-прогон-преп; ПАК-11 ЗАВЕРШЁН**; **D39.16 = арх-проход ЗАЛЕНДЕН: майнер-данные (百家姓/Палладий/частицы/суффиксы) вынесены в `internal/lang`+`configs/langpacks/` [граница компиль-enforced, байт-точно — парити EXACT, golden байт-идентичен]; FL-1/2/3+тест-путь; РАТИФИЦИРОВАНЫ 3 дивергенции: language.Tag ОТВЕРГНУТ [CLDR-инстабильность в вердиктах], DC-чекеры+снапшот-фолд DEFER [майнер offline/нейтральность]; общность структурно доказана; промт отработан→архив**; **D39.15 = директива владельца: изоляция языковых данных из `pipeline/` [движок общий · `internal/lang/`+`configs/langpacks/` данными-файлами · `x/text/language.Tag` вместо `isRuTarget` · снапшот-folded как промпты · алгоритмы не трогать]; архитектурный проход ПЕРЕД R1 [ре-секвенс]; поведенчески нейтрально zh→ru [golden байт-стабилен]; не минор-хант/не rewrite-ради-rewrite; промт `BACKEND_ARCH_CLEANUP_SESSION_PROMPT.md` 5-фазный [ресёрч→синтез→фикс-лист→исполнение→ревью]; ja→ru = тест общности §B5 позже**; **D39.14 = пак-11 continuation ПРИНЯТ и залендён [R2 майнер паритет-EXACT · R3 банкнота 12-точек · R4 DC-чекеры+export--pairs · R5 арм-конфиги · R1-аддитив; рубеж-2 D39.13-долга погашен: 3 MINOR/PARTIAL-находки [FL-1 bankTokenBudget-фолд · FL-2 banknote-телеметрия-резюм · FL-3 emission-top-N-кап], все латентны → R1-продолжение; R1 драйвер-свитч резидуал; clean bill по деньгам/wire/порту]**; **D39.13 = пак-11 Block A ПРИНЯТ и залендён [WS2 целиком (вкл. src→dst-блок редактора = закрытие D30.1) + WS1a/b/c-инфра + WS4-ядро; 2 девиации ратифицированы; резидуал R1–R5 в PACK11_REPORT; промт остаётся активным; рубеж-2 приёмки — долг]**; **D39.12 = план `architecture/11-implementation-plan.md` РАТИФИЦИРОВАН [три рубежа: ревью-2 F1–F11 → 7-агентный адверсариал → подтверждающий пас; пер-волновой снапшот + инвариант секвенирования = «переоплата ОДНА»; EditCeilingOut=3200; лемматизатор дефолт B; 4 гейтнутых арма]; бэкенд-пак ЕДИНОЙ сессией — `BACKEND_PLAN11_SESSION_PROMPT.md`**; **D39.11 = директива владельца: стройка ТОЛЬКО после дизайн-синтез сессии с верифицированными алгоритмами; хендофф №5**; **D39.10 = exp16: WHICH подтверждена [V-C recall 0.97, $0] / WHAT→банкнота+Палладий [canon-recovery <70%], 9B-споттер не нужен, Z1→облако, ре-аудит exp14b чист [errata не требуется]; исследовательская программа ЗАВЕРШЕНА → бэкенд-пак слоёв 1+4**; **D39.9 = Q4a: верность НЕ на translate-стадии [flash уже верен, pro ×3.67 не ратифицирован], смысл ломает glm-РЕДАКТОР [свап-арм приоритетен], DET-пол 0.261; exp14b-правила дефектны → $0-ре-аудит в exp16**; **D39.8 = слепое чтение: планка не пройдена никем, дефекты распределены [читатель подтвердил floor-null], битва качества → дефект-классы [时辰-юниты, числа, gender-enforce, стих, регистр]**; **D39.7 = exp15 залендён REV.2: «граница вредит» отозван [артефакт окна судьи], все когезия-эффекты под floor 0.126, «sequential нигде не лучше» LOO-stable [не ратифицированный зелёный свет], фертильность 1.20/0.39, $12.79/$15; гейт = слепое чтение владельца**; **D39.6 = research/20 банк-майнинг залендён [детектор-лестница V-A/B/C, алиас-ярусы, banknote-v1 с 12 гейт-точками, §B5 плагины P1–P6, §D-пре-рег]; полигон-стадия после exp15**; **D39.5 = пак-1.5+F1–F9 залендены [tmctl export с manifest/drift-гардами · fold-first санитайзер · bounded глосс · пример zh-ru]; экстракция полигона впредь через tmctl export**; **D39.4 = адверсариал-долг ПОГАШЕН [8 осей: 0 CRIT/1 HIGH/6 MED], пак-1.5 придержан под фикс-лист F1–F9 в §T4**; **D39.3 = пакет банк-майнинга W1.5 [диспозиция H15/V4-п4] ратифицирован, research/20-промт с HOLD до фриза exp15**; **D39.2 = трек-A пак-1 залендён `d3f6b34`: trust-gated suppressor [терм-дрейф закрыт в КОДЕ] + export-contract нормализация + pair-сеам + реестры + quality-report; адверсариал-долг за 529**; **D39.1 = research/19 залендён: конфликт exp14↔research/18 разрешён [эмиссия≠окно≠связность], Go-спека чанкера+когезии, волновая архитектура W0–W3 [вводная владельца: параллелизм], пре-рег эмпирики Q0–Q5; оркестратор-ревью снято владельцем**; **D39 = АРХ-РЕСЕТ: синк-аудит сломанного телефона [65 находок, 0 refuted, `08-sync-audit-ledger.md`] + целевая 7-слойная архитектура [`09-target-architecture.md`] + фазовый план [трек A build-now ∥ трек B ресёрч, пере-прогон после]; формализует фантомный «D35.7» как цель слоя 1; терм-дрейф код-корень жив [сид-воркэраунд]; редактор 4-мандата-full-regen = причина инверсий**; D35 = пивот качество-первым; D36 = приёмка research/18; D37 = приёмка exp14: претензия-1=промпт-рычаг; D38 = приёмка exp14b: «только gpt-5.4» ОПРОВЕРГНУТО [mistral/deepseek-pro чинят], фронтир в дефолт НЕ нужен, корень терм-дрейфа = статус-draft сида; **D38.1 = слепой h2h P1a↔F-disc залендён [ChatGPT «фронтир сильнее» / Claude «near-parity» расходятся, СХОДЯТСЯ: дефекты = инфра] → РАЗВОРОТ «мерить→строить», выдан BACKEND_INFRA_PACK; D38.2 = консолидация/док-гигиена: спент exp-скрипты в `eval/exp12|13|14|14b/`, статус-доки+.puml актуализированы; D38.3 = инфра-пак читаемости залендён (санитайзер-классы strip+export + число/omission-гард), верифицирован исполнением [build/vet/test зелёные, 3 span-дефекта запинены]; D38.4 = аудит полноты exp14/14b: чэнъюй-рычаг забыт near-term→дописан в дискурс-промпт; inversion-гард D37 §2г не строился (число/omission ≠ полярность)→editor-swap = защита от инверсий (приоритет↑); нарезка = re-run-арм; гард-тумблер вкл. при resnapshot; D38.5 = компоненты resnapshot залендены+верифицированы [reseed-сид eb409f2 · дискурс+чэнъюй editor v3 0ac5f7a с few_shot-тумблером] → следующий шаг единый resnapshot + пере-прогон**). Ответ на вопросы бэкенд-сессии (PROGRESS §«Вопросы от бэкенда», §«[НУЖНО РЕШЕНИЕ] перед Фазой 1» — с ревизии D31 в `archive/PROGRESS-2026-07-04-10.md`) и полигона (think-режим). Каждое решение прошло адверсариальную панель из 3 критиков (research / экономика / исполнимость в коде Фазы 0); ни одно не отклонено, все уточнены. Это **контракт Фазы 1** — бэкенд исполняет отсюда; при конфликте с буквой 01-decisions/02-mvp-plan — источник истины здесь (потом сольём в основные доки). @@ -563,6 +563,16 @@ exp14b (D37-мандат) прогнал батарею DET-смысл-трап **Ратифицированные девиации/флаги (приняты):** пер-волновой `--resnapshot` СУРГИЧЕН (не all-or-nothing — прямое следствие пер-волнового снапшота §1(в); `TestRunnerSnapshotPinning` 4→3) · `escMu` сериализует эскалации через LLM-вызов (осознанный трейд: точность soft-cap > параллелизм редких эскалаций) · Ш-2 расширен на chunker/estimator (тот же silent-drift класс). **Пак-11 ЗАВЕРШЁН** (Block A + continuation + арх-проход + R1); промт `BACKEND_PLAN11` отработан → архив. **Очередь:** пере-прогон-преп (R1-FL-A/B/C mining-wiring + единый resnapshot §8 D30.9) → пере-прогон 3–10 глав (армы glm/mistral/deepseek-pro; live mining-раунд с подписью) → чтение владельца (планка 2 претензии). +## D39.19 — Пере-прогон-преп ЗАЛЕНДЕН: 6 целевых фиксов (R1-FL-A/B/C · echo-сплит · read-model naming · ниты) + §8-манифест полон; golden version-only; gender-signoff закрыт (19.07, оркестратор №7). ✅ + +Бэкенд-сессия исполнила `BACKEND_RERUN_PREP_SESSION_PROMPT` — все 6 пунктов + верификация §8-манифеста + свой 4-линзовый адверсариал (clean bill, 0 CRIT/MAJOR). **Залендено:** (1) **R1-FL-A** — CLI обрабатывает `WaveSignatureStop` → **exit 3** (distinct от 2-flagged/1-крэш, переживает `%w`), рендер terms/signature-path/resume-guidance, оверклейм-комменты выправлены; (2) **R1-FL-B** — **reject-set** (`Book.MinedRejects` yaml, не в BriefHash → не снапшот-двигающ; `MineBank(rejects)` дропает отклонённый терм С кластером [`markEmitted` против representative-shift re-fire], nil = байт-идентичный no-op → парити цел); стоп очищается когда каждый терм промоутнут ИЛИ отклонён — **livelock/W1.5-UX §10-1 закрыт**; (3) **R1-FL-C** — `xTextVersion="v0.38.0"` в `memoryNormAlgoVersion` (pin-константа: `ReadBuildInfo` пуст под `go test`; drift-чек `TestMemnormXTextVersionPin` против go.mod) — единственная снапшот-двигающая правка; (4) **echo-сплит** `echo_draft_rate`/`echo_edit_rate` (re-derive-костыль снят; edit-числитель gated `DispFlagged`); (5) **read-model naming** (chunk/total_chunks → unit-семантика Go+JSON; полигон-экстрактор читает per-record `chunk_idx`/`source`, не переименованные тоталы — не сломан); (6) **ниты** (мёртвый `minedToCandidates` снесён греп-подтверждённо; jobs.status advisory-note; escMu не тронут; HTTP-concurrency уже покрыт `TestWaveParallelWorkersMoneyConserved` через реальный транспорт). + +**Приёмка — прямая верификация исполнением** (локальные целевые фиксы — тяжёлый воркфлоу не нужен; сессия сама прогнала 4-линзовый адверсариал): build/vet/test -race зелёные (сам); **независимый masked-golden = 0 изменённых строк после маскировки хешей** — golden сдвинулся ЧИСТО на хеш-значениях (x/text-версия в `memoryNormVersion`), wire-тела/вердикты/final_text БАЙТ-ИДЕНТИЧНЫ (сильнейшее подтверждение version-only; 180 «version-only» сессии = все хеш-строки); все пины проходят (exit-код-контракт · reject-suppress · x/text-drift-pin · echo-сплит [draft 0.5/edit 0 на dropped-echo] · **парити EXACT** [reject nil no-op] · mined-delta-base-stable · golden); спот-чтение несущих диффов (x/text pin+drift, reject cluster-touch+markEmitted, echo-сплит). **§8-манифест:** сессия сверила все live снапшот-фолды — полны (memmatch/reseed/mined/gender-данные/sanitizer/editor/segmentation/chunker-v5/src→dst/banknote/langpack/per-wave/**+x/text**); DEFER-строки (DC-пакет-данные, promote 甲乙丙丁/四代族长) корректно не-фолдятся (не построены). **Док-правка оркестратора:** x/text-строка добавлена в план §8 (сессия флагнула отсутствие — моя зона; выправлено). Дисциплина «не упарываться на миноры» соблюдена (ниты лёгкие, escMu не тронут, снос только греп-мёртвого). + +**Gender-signoff карты exp16 ЗАКРЫТ (владелец «male ок», оркестратор проставил корректно):** male 10 name-персон (方源/古月/方正/人祖/花酒行者/古月-клан) · female 沈嬷嬷 (нянька) + 沈翠 (KWIC разрешил: служанка, дочь няньки — не бланк-male) · hidden 白凝冰 · n/a title/term/place + 困境 (имя стаи). Карта (dst-канон+пол) готова к сид-дельте. + +**ПАК-11 + преп ЗАВЕРШЕНЫ; бэкенд-код готов к пере-прогону.** Промт препа отработан → архив. **Очередь:** **пере-прогон** (операционная сессия: ОДИН `--resnapshot` fresh-translate + langpack+contrast+подписанная карта+армы редактора glm/mistral/deepseek-pro; риг-стандарт D39.7; finish=stop) → чтение владельца (планка 2 претензии). **Гейт: ре-чек прайса DeepSeek у катовера слагов 24.07 ДО платных вызовов.** + ## D39.18 — Blast-radius edit-единицы: c-lite (редактура чистого остатка) залендён (`488c8d4`); + 4 read-модель-фикса пост-лендинг-ревью (19.07, оркестратор №7). ✅ **Product-решение владельца по blast-radius (эскалировано D39.17-errata):** в волновой модели флаг одного черновик-члена бланкал ВСЮ edit-единицу (≈1–2 чанка ≈ глава для мелких глав), отбрасывая оплаченные драфты хороших соседей — регресс от per-chunk модели. Опции: (a) accept · (b) экспорт сырых хороших · (c) редактура чистого остатка. **Ратифицирован (c) с fallback (b)** (моя рекомендация: (c) не намного сложнее (b) — склейка хороших `draftParts` уже собиралась; деградирует до «потеряли один битый чанк» + сохранённая редактура хороших). diff --git a/docs/architecture/11-implementation-plan.md b/docs/architecture/11-implementation-plan.md index 31a830eb..6beee123 100644 --- a/docs/architecture/11-implementation-plan.md +++ b/docs/architecture/11-implementation-plan.md @@ -827,6 +827,9 @@ WS5-omission-бэкстоп компенсирует WS2-зону деграда `memoryMatchVersion`; §5(в)/§2(в)); DC-чекеры → `StyleCheckVersion`; promote 甲乙丙丁/四代族长 → `MemoryVersion`. - WS6: editor-model per-арм → `stageSnap.Model` (разные армы = разные снапшоты, но это РАЗНЫЕ прогоны, не переоплата одного). +- **R1-FL-C (D39.17/D39.19):** `xTextVersion` (`x/text/norm` Unicode-редакция, независима от stdlib `unicode.Version`) + → `memoryNormAlgoVersion` → `memoryNormVersion` → `MemoryVersion` (обе волны) → `snapshot_draft`+`snapshot_edit`. + Иначе `go get -u golang.org/x/text` тихо сдвинул бы NFKC. Pin-константа, drift-чек против go.mod. - **Структурное (ревью-2 F9):** сама пер-волновая реструктуризация `snapshotID()` (два `MemoryVersion` base/enriched + два снапшота W1/W2 вместо одного book-global, §1(в)) = снапшот-инвалидация — ОЖИДАЕМАЯ строка golden re-capture (структура хеша меняется даже при идентичном контенте). diff --git a/docs/BACKEND_RERUN_PREP_SESSION_PROMPT.md b/docs/archive/prompts/BACKEND_RERUN_PREP_SESSION_PROMPT_2026-07-19.md similarity index 97% rename from docs/BACKEND_RERUN_PREP_SESSION_PROMPT.md rename to docs/archive/prompts/BACKEND_RERUN_PREP_SESSION_PROMPT_2026-07-19.md index d93f6111..8e30602b 100644 --- a/docs/BACKEND_RERUN_PREP_SESSION_PROMPT.md +++ b/docs/archive/prompts/BACKEND_RERUN_PREP_SESSION_PROMPT_2026-07-19.md @@ -1,3 +1,5 @@ +> **⟶ ОТРАБОТАН → все 6 фиксов залендены, ратифицирован D39.19 (19.07). golden version-only, парити EXACT, §8-манифест полон. Бэкенд готов к пере-прогону. Архивная копия.** + # Промт: сессия БЭКЕНД — пере-прогон-преп (mining-wiring фиксы · echo-сплит · read-model чистка · готовность к единому resnapshot), 2026-07-19 > **Место в очереди (пост-D39.18):** ПАК-11 ЗАВЕРШЁН (Block A → continuation → арх-проход → R1 драйвер-свитч →