Land bank quality both stages as D39.69: head-aware series batching, classifier-first typing, data-driven decl stemmer with neuter, golden verdict-neutral, fix list and owner questions queued
This commit is contained in:
parent
4de87cb651
commit
f15e35202a
31 changed files with 1791 additions and 242 deletions
|
|
@ -320,6 +320,29 @@ type TerminologyGate struct {
|
|||
// PromptPath is the RESOLVED role prompt (`<prompts root>/<pair>/terminologist.md`), filled by
|
||||
// LoadPipeline by the ordinary role convention. Not a config key.
|
||||
PromptPath string `yaml:"-"`
|
||||
// ClassifyTypes turns on the §2 type-classifier PHASE: before the terminologist renders, a focused pass
|
||||
// over the same batches re-derives each candidate's type (the draft heuristic is wrong 12–22%). The
|
||||
// corrected type routes conformance, primes the wire block and is the banked type, so a realia surface
|
||||
// mistyped as a name is no longer FORCED to a transliteration. Off → the heuristic type is used exactly as
|
||||
// before, byte-identically. The phase reuses the batch sizing and target-script above.
|
||||
ClassifyTypes bool `yaml:"classify_types"`
|
||||
// ClassifyModel is the classifier phase's model; empty → Model. Classification is cheaper than a render,
|
||||
// so an operator may point it at a smaller model.
|
||||
ClassifyModel string `yaml:"classify_model"`
|
||||
// ClassifyBudgetUSD is the classifier phase's OWN book-wide ceiling, separate from BudgetUSD so a classify
|
||||
// overrun cannot starve the render phase and the two costs stay attributable apart. >0 required when on.
|
||||
ClassifyBudgetUSD float64 `yaml:"classify_budget_usd"`
|
||||
// ClassifyPromptPath is the RESOLVED classifier prompt (`<prompts root>/<pair>/classifier.md`), filled by
|
||||
// LoadPipeline. Not a config key.
|
||||
ClassifyPromptPath string `yaml:"-"`
|
||||
}
|
||||
|
||||
// ClassifierModel resolves the classifier phase's model — its own if set, else the render model.
|
||||
func (g TerminologyGate) ClassifierModel() string {
|
||||
if g.ClassifyModel != "" {
|
||||
return g.ClassifyModel
|
||||
}
|
||||
return g.Model
|
||||
}
|
||||
|
||||
// RepairGate controls the addressable-defect repair sub-step (pack-16, D39.24): when enabled, a FINAL
|
||||
|
|
@ -1046,6 +1069,25 @@ func LoadPipeline(path string, models *Models, pair string, labels []string) (*P
|
|||
bad("gates.terminology is enabled but its role prompt is missing — expected %s (authored in the pair's own language, like the other role prompts)", p.Gates.Terminology.PromptPath)
|
||||
}
|
||||
}
|
||||
// The §2 classifier phase: same structural contract as the render phase — an enabled phase must be able
|
||||
// to fire (a resolvable model, a non-additive provider, its own budget, its own prompt).
|
||||
if tg.ClassifyTypes {
|
||||
cm := tg.ClassifierModel()
|
||||
if _, ok := models.Models[cm]; !ok {
|
||||
bad("gates.terminology.classify_model %q is not defined in models.yaml", cm)
|
||||
} else if models.providerReasoning(cm) == "additive" {
|
||||
bad("gates.terminology.classify_model %q sits on an ADDITIVE-billing provider and this phase carries no reasoning_max_tokens to reserve it with — the spend ceiling would be blind (D6.2/D13.6); pick a subset-billing model", cm)
|
||||
}
|
||||
if tg.ClassifyBudgetUSD <= 0 {
|
||||
bad("gates.terminology.classify_budget_usd must be > 0 when classify_types is on (a phase that can never spend is a silent no-op)")
|
||||
}
|
||||
if pair != "" {
|
||||
p.Gates.Terminology.ClassifyPromptPath = promptConventionPath(promptsRoot, pair, classifierRoleName)
|
||||
if _, err := os.Stat(p.Gates.Terminology.ClassifyPromptPath); err != nil {
|
||||
bad("gates.terminology.classify_types is on but its prompt is missing — expected %s (authored in the pair's own language, like the other role prompts)", p.Gates.Terminology.ClassifyPromptPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(problems) > 0 {
|
||||
return nil, fmt.Errorf("config %s:\n - %s", path, strings.Join(problems, "\n - "))
|
||||
|
|
@ -1062,3 +1104,7 @@ const repairPromptDirName = "repair"
|
|||
// `<prompts root>/<pair>/<role>.md` convention every stage role uses — it is a role of the pair's prompt
|
||||
// pack, not a second prompt namespace.
|
||||
const terminologyRoleName = "terminologist"
|
||||
|
||||
// classifierRoleName is the ROLE the §2 type-classifier prompt is keyed by, resolved through the same
|
||||
// <prompts root>/<pair>/<role>.md convention as every other role prompt.
|
||||
const classifierRoleName = "classifier"
|
||||
|
|
|
|||
|
|
@ -10,3 +10,4 @@ ru unverified_marker ⟨проверить⟩
|
|||
ru gender_male (муж. — мужские родовые формы)
|
||||
ru gender_female (жен. — женские родовые формы)
|
||||
ru gender_hidden (пол СКРЫТ до раскрытия — избегай родовых форм; при неизбежности — мужские)
|
||||
ru gender_neuter (ср. — средний род: используй родовые формы среднего рода)
|
||||
|
|
|
|||
|
|
@ -184,3 +184,47 @@ translit_interjection ауч
|
|||
translit_interjection упс
|
||||
translit_interjection кья
|
||||
translit_interjection десу
|
||||
# decl_suffix: the productive Russian inflectional endings the decl-aware post-check stems a word by
|
||||
# (bank-quality §3, primitive C). It lets dstFormPresent accept a term rendered in an oblique case
|
||||
# («Фан Юаню» for «Фан Юань») WITHOUT the seed listing every form, closing the measured decl-142/142-null
|
||||
# false-miss noise. TARGET data — the suffix set is ru-specific; the stemmer ALGORITHM (strip the longest
|
||||
# listed ending that leaves a stem ≥ min length) stays generic Go and a target with no decl_suffix rows
|
||||
# gets no stemming (inert). It is a FLAGGER aid, biased to ACCEPT declensions; it never gates. Ordered
|
||||
# longest-first in code, so listing order here is free.
|
||||
decl_suffix ами
|
||||
decl_suffix ями
|
||||
decl_suffix ого
|
||||
decl_suffix его
|
||||
decl_suffix ому
|
||||
decl_suffix ему
|
||||
decl_suffix ыми
|
||||
decl_suffix ими
|
||||
decl_suffix ах
|
||||
decl_suffix ях
|
||||
decl_suffix ов
|
||||
decl_suffix ев
|
||||
decl_suffix ом
|
||||
decl_suffix ём
|
||||
decl_suffix ем
|
||||
decl_suffix ой
|
||||
decl_suffix ей
|
||||
decl_suffix ый
|
||||
decl_suffix ий
|
||||
decl_suffix ая
|
||||
decl_suffix яя
|
||||
decl_suffix ое
|
||||
decl_suffix ее
|
||||
decl_suffix ым
|
||||
decl_suffix им
|
||||
decl_suffix ых
|
||||
decl_suffix их
|
||||
decl_suffix ую
|
||||
decl_suffix юю
|
||||
decl_suffix а
|
||||
decl_suffix я
|
||||
decl_suffix у
|
||||
decl_suffix ю
|
||||
decl_suffix е
|
||||
decl_suffix ы
|
||||
decl_suffix и
|
||||
decl_suffix о
|
||||
|
|
|
|||
|
|
@ -395,6 +395,7 @@ type InjectionTexts struct {
|
|||
GenderMale string // " (муж. — …)" DC3 gender directive (leading space significant)
|
||||
GenderFemale string // " (жен. — …)"
|
||||
GenderHidden string // " (пол СКРЫТ …)"
|
||||
GenderNeuter string // " (ср. — …)" — a neuter entity/creature (row 84); "" for a target with no row
|
||||
}
|
||||
|
||||
// HasData reports whether the target has injection texts (its header is present). The renderers use it to
|
||||
|
|
@ -452,6 +453,8 @@ func parseInjection(b []byte) (map[string]*InjectionTexts, error) {
|
|||
t.GenderFemale = val
|
||||
case "gender_hidden":
|
||||
t.GenderHidden = val
|
||||
case "gender_neuter":
|
||||
t.GenderNeuter = val
|
||||
default:
|
||||
return nil, fmt.Errorf("line %d: unknown injection key %q", i+1, key)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,3 +82,17 @@ func IsCJKScriptLang(lng string) bool {
|
|||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// SeriesMorphology reports whether a SOURCE language forms rune-morpheme SERIES the terminologist should
|
||||
// co-batch (甲等/乙等/丙等 — grades sharing the head 等), and whether that shared head is final. Series apply
|
||||
// to DENSE (CJK) scripts, where one rune ≈ one morpheme so a single-rune difference is a real minimal pair;
|
||||
// in an alphabetic source care/core differ in one letter by coincidence, so the channel stays off. CJK noun
|
||||
// compounds are modifier-HEAD, so the generic word is the trailing rune(s): head-final. A linguistic constant
|
||||
// like cjkScriptNames, not a pair/book branch — the one place to revisit if a non-head-final dense script is
|
||||
// ever added.
|
||||
func SeriesMorphology(lng string) (enabled, headFinal bool) {
|
||||
if IsCJKScriptLang(lng) {
|
||||
return true, true
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
|
|
|
|||
101
backend/internal/lang/stemmer.go
Normal file
101
backend/internal/lang/stemmer.go
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
package lang
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// stemmer.go: the conservative TARGET stemmer (bank-quality §3, primitive C). It strips one inflectional
|
||||
// ending off a word to get its stem, so a decl-aware check accepts an oblique case («мечом») against its
|
||||
// nominative («меч») WITHOUT the seed listing every form — the fix for the decl-142/142-null false-miss
|
||||
// noise, and the backstop the checker omission-detector shares. The ALGORITHM is generic; the ending REGISTRY
|
||||
// is target data (decl_suffix in data/target-<tgt>.txt), so a target with none gets an inert stemmer (exact
|
||||
// match only). A FLAGGER aid, biased to ACCEPT declensions; it never gates.
|
||||
|
||||
// minStemRunes is the shortest stem a suffix strip may leave. Below it a "stem" is too short to distinguish
|
||||
// two words (стripping «а» off «дома» is fine → «дом»; stripping it off «яма» to «ям» is not worth trusting),
|
||||
// so the ending is not stripped and the word stands whole. Conservative on the side of NOT conflating words.
|
||||
const minStemRunes = 3
|
||||
|
||||
// TargetStemmer strips a registry ending off a word. The zero value is a valid inert stemmer (no endings →
|
||||
// Stem is the identity → SameStem is exact equality), which is what a target with no decl_suffix data gets.
|
||||
type TargetStemmer struct {
|
||||
suffixes []string // registry endings, longest-first
|
||||
}
|
||||
|
||||
// NewTargetStemmer builds the stemmer from a target's decl_suffix registry. No registry → an inert stemmer.
|
||||
func NewTargetStemmer(tc TargetChecks) TargetStemmer {
|
||||
sfx := append([]string(nil), tc.List("decl_suffix")...)
|
||||
// Longest-first so Stem strips the most specific ending (е.g. «ому» before «у»); ties by bytes for a
|
||||
// stable, deterministic order.
|
||||
sort.SliceStable(sfx, func(i, j int) bool {
|
||||
if a, b := len([]rune(sfx[i])), len([]rune(sfx[j])); a != b {
|
||||
return a > b
|
||||
}
|
||||
return sfx[i] < sfx[j]
|
||||
})
|
||||
return TargetStemmer{suffixes: sfx}
|
||||
}
|
||||
|
||||
// Enabled reports whether the stemmer carries any endings — false for a target with no decl_suffix data, so a
|
||||
// consumer can skip the whole stem branch.
|
||||
func (s TargetStemmer) Enabled() bool { return len(s.suffixes) > 0 }
|
||||
|
||||
// Stem returns word with its longest registry ending removed, provided the remaining stem stays ≥
|
||||
// minStemRunes; otherwise the word is returned unchanged. Case-insensitive (folded to lower), so a match is
|
||||
// case-blind. An empty or all-short word returns folded-as-is.
|
||||
func (s TargetStemmer) Stem(word string) string {
|
||||
w := strings.ToLower(word)
|
||||
rs := []rune(w)
|
||||
if len(rs) < minStemRunes {
|
||||
return w
|
||||
}
|
||||
for _, suf := range s.suffixes {
|
||||
sr := []rune(suf)
|
||||
if len(rs)-len(sr) < minStemRunes {
|
||||
continue // stripping this ending would leave too short a stem
|
||||
}
|
||||
if strings.HasSuffix(w, suf) {
|
||||
return string(rs[:len(rs)-len(sr)])
|
||||
}
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
// SameStem reports whether two words share a stem under this stemmer — the decl-aware equality a consumer
|
||||
// uses to accept one form of a word against another. Two identical words trivially match; the value the
|
||||
// stemmer adds is matching «мечом»/«меча»/«меч». Never true for an inert stemmer beyond exact equality.
|
||||
func (s TargetStemmer) SameStem(a, b string) bool {
|
||||
la, lb := strings.ToLower(a), strings.ToLower(b)
|
||||
if la == lb {
|
||||
return true
|
||||
}
|
||||
if !s.Enabled() {
|
||||
return false
|
||||
}
|
||||
return s.Stem(la) == s.Stem(lb)
|
||||
}
|
||||
|
||||
// TokenizeWords splits a target string into its whole words (maximal letter runs), lower-cased. Script-
|
||||
// generic (any Unicode letter), so it needs no per-language branch. Used to match a declined multi-word term
|
||||
// head against the output.
|
||||
func TokenizeWords(s string) []string {
|
||||
var out []string
|
||||
var cur []rune
|
||||
flush := func() {
|
||||
if len(cur) > 0 {
|
||||
out = append(out, string(cur))
|
||||
cur = cur[:0]
|
||||
}
|
||||
}
|
||||
for _, r := range strings.ToLower(s) {
|
||||
if unicode.IsLetter(r) {
|
||||
cur = append(cur, r)
|
||||
continue
|
||||
}
|
||||
flush()
|
||||
}
|
||||
flush()
|
||||
return out
|
||||
}
|
||||
57
backend/internal/lang/stemmer_test.go
Normal file
57
backend/internal/lang/stemmer_test.go
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
package lang
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestTargetStemmerRuDeclension(t *testing.T) {
|
||||
s := NewTargetStemmer(TargetChecksFor("ru"))
|
||||
if !s.Enabled() {
|
||||
t.Fatal("ru ships a decl_suffix registry, so the stemmer must be enabled")
|
||||
}
|
||||
// Common-noun oblique cases collapse to one stem — the measured decl-142/142-null case.
|
||||
groups := [][]string{
|
||||
{"меч", "меча", "мечом", "мечи", "мечу"},
|
||||
{"апертура", "апертуры", "апертуру", "апертурой", "апертуре"},
|
||||
{"старейшина", "старейшину", "старейшины", "старейшиной"},
|
||||
{"источник", "источника", "источнику", "источнике"},
|
||||
{"гора", "горы", "гору", "горе", "горой"},
|
||||
}
|
||||
for _, g := range groups {
|
||||
for _, w := range g {
|
||||
if !s.SameStem(g[0], w) {
|
||||
t.Errorf("%q and %q must share a stem (%q vs %q)", g[0], w, s.Stem(g[0]), s.Stem(w))
|
||||
}
|
||||
}
|
||||
}
|
||||
// It must NOT conflate distinct words: a shared prefix that is not an inflection.
|
||||
for _, pair := range [][2]string{{"меч", "мечта"}, {"гора", "город"}, {"дом", "домна"}} {
|
||||
if s.SameStem(pair[0], pair[1]) {
|
||||
t.Errorf("%q and %q are different words and must not share a stem (%q vs %q)", pair[0], pair[1], s.Stem(pair[0]), s.Stem(pair[1]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTargetStemmerInertWithoutRegistry(t *testing.T) {
|
||||
var s TargetStemmer // zero value = a target with no decl_suffix data
|
||||
if s.Enabled() {
|
||||
t.Fatal("a stemmer with no registry must be inert")
|
||||
}
|
||||
if s.SameStem("меча", "меч") {
|
||||
t.Fatal("an inert stemmer must fall back to EXACT equality only")
|
||||
}
|
||||
if !s.SameStem("меч", "меч") {
|
||||
t.Fatal("an inert stemmer must still match identical words")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenizeWords(t *testing.T) {
|
||||
got := TokenizeWords("Фан Юаню, к Горе!")
|
||||
want := []string{"фан", "юаню", "к", "горе"}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("tokens = %v, want %v", got, want)
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("token %d = %q, want %q", i, got[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
75
backend/internal/membank/memdecl_test.go
Normal file
75
backend/internal/membank/memdecl_test.go
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
package membank
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"textmachine/backend/internal/lang"
|
||||
"textmachine/backend/internal/store"
|
||||
"textmachine/backend/internal/text"
|
||||
)
|
||||
|
||||
func present(t *testing.T, e *entry, output string, s lang.TargetStemmer) bool {
|
||||
t.Helper()
|
||||
norm := text.NormalizeTargetForm(output)
|
||||
return dstFormPresent(e, []rune(norm), lang.TokenizeWords(norm), s)
|
||||
}
|
||||
|
||||
// TestDstFormPresentAcceptsDeclension is the §3 pin: a term rendered in an oblique case is ACCEPTED against
|
||||
// its nominative even when the seed listed no decl forms (the measured 142/142-null noise), while a term
|
||||
// genuinely absent is still a miss. Multi-word terms match component-wise, whichever word inflected.
|
||||
func TestDstFormPresentAcceptsDeclension(t *testing.T) {
|
||||
s := lang.NewTargetStemmer(lang.TargetChecksFor("ru"))
|
||||
single := &entry{dst: "меч"}
|
||||
if !present(t, single, "он поднял свой мечом высоко", s) {
|
||||
t.Fatal("an oblique case of a single-word term must be accepted (мечом ~ меч)")
|
||||
}
|
||||
if present(t, single, "он поднял свой клинок высоко", s) {
|
||||
t.Fatal("a genuinely absent term must still be a miss")
|
||||
}
|
||||
multi := &entry{dst: "гора Цинмао"}
|
||||
if !present(t, multi, "они стояли у горы Цинмао", s) {
|
||||
t.Fatal("a multi-word term with the head inflected must be accepted (горы Цинмао ~ гора Цинмао)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDstFormPresentInertWithoutStemmer guards the generality path: a target with no decl_suffix registry
|
||||
// (an inert stemmer) keeps EXACT-match behaviour, so it never falsely accepts a declined form.
|
||||
func TestDstFormPresentInertWithoutStemmer(t *testing.T) {
|
||||
var inert lang.TargetStemmer
|
||||
e := &entry{dst: "меч"}
|
||||
if present(t, e, "он поднял мечом", inert) {
|
||||
t.Fatal("without a stemmer the declined form must NOT match")
|
||||
}
|
||||
if !present(t, e, "он поднял меч", inert) {
|
||||
t.Fatal("the exact nominative must still match with no stemmer")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenderConstraintNoteNeuter(t *testing.T) {
|
||||
tx := lang.InjectionTextsFor("ru")
|
||||
if tx.GenderNeuter == "" {
|
||||
t.Fatal("ru must ship a gender_neuter directive")
|
||||
}
|
||||
for _, g := range []string{"neuter", "n"} {
|
||||
if got := genderConstraintNote(g, tx); got != tx.GenderNeuter {
|
||||
t.Fatalf("gender %q must map to the neuter directive, got %q", g, got)
|
||||
}
|
||||
}
|
||||
if genderConstraintNote("xyz", tx) != "" {
|
||||
t.Fatal("an unknown gender must produce no directive (the row-84 silent no-op it once was)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenderVocabViolations(t *testing.T) {
|
||||
entries := []store.GlossaryEntry{
|
||||
{Src: "甲", Dst: "А", Gender: "male"},
|
||||
{Src: "乙", Dst: "Б", Gender: "neuter"}, // now valid
|
||||
{Src: "丙", Dst: "В", Gender: ""}, // absent is fine
|
||||
{Src: "丁", Dst: "Г", Gender: "Male"}, // wrong case → a silent no-op today, must flag
|
||||
{Src: "戊", Dst: "Д", Gender: "мужской"}, // a typo → must flag
|
||||
}
|
||||
bad := GenderVocabViolations(entries)
|
||||
if len(bad) != 2 {
|
||||
t.Fatalf("exactly the two off-vocabulary genders must flag, got %d: %v", len(bad), bad)
|
||||
}
|
||||
}
|
||||
|
|
@ -174,6 +174,10 @@ type Bank struct {
|
|||
// they sit beside the automaton rather than inside it: nothing here can put them in keyOwners.
|
||||
voices []store.VoiceProfile
|
||||
pairs []store.AddressPair
|
||||
// stemmer is the target-language decl stemmer (bank-quality §3): it lets the post-check accept an oblique
|
||||
// case of a term against its nominative without the seed listing every form. The zero value is inert (a
|
||||
// target with no decl_suffix registry), so a book that ships none keeps exact-match behaviour.
|
||||
stemmer lang.TargetStemmer
|
||||
}
|
||||
|
||||
// PickedEntry is one selected record for a chunk with its firing key and disposition.
|
||||
|
|
@ -250,6 +254,9 @@ type BankInput struct {
|
|||
// hashing them would re-bill a book for authoring a profile that changed no byte the model sees.
|
||||
// The moment they are injected, they must fold — the D39.42 п.3 class, in the other direction.
|
||||
InjectVoice bool
|
||||
// TargetStemmer is the decl-aware post-check stemmer (bank-quality §3). Zero value → inert (exact match
|
||||
// only), so a caller that supplies none keeps the pre-§3 behaviour byte-for-byte.
|
||||
TargetStemmer lang.TargetStemmer
|
||||
}
|
||||
|
||||
// Materialize builds a Bank from the book's stored glossary rows alone — the pre-pack-19 form, kept for
|
||||
|
|
@ -265,7 +272,7 @@ func Materialize(rows []store.GlossaryEntry, gateOn bool) *Bank {
|
|||
// the approved glossary OR to the deterministic machinery is a loud --resnapshot (F1).
|
||||
func MaterializeBank(in BankInput, gateOn bool) *Bank {
|
||||
rows := in.Rows
|
||||
b := &Bank{keyOwners: map[string][]int{}, voices: in.Voices, pairs: in.Pairs}
|
||||
b := &Bank{keyOwners: map[string][]int{}, voices: in.Voices, pairs: in.Pairs, stemmer: in.TargetStemmer}
|
||||
var allKeys []string
|
||||
seenKey := map[string]bool{}
|
||||
|
||||
|
|
@ -383,6 +390,7 @@ func ComputeVersionScopedIn(in BankInput, gateOn, excludeMined bool) string {
|
|||
h.Write([]byte(text.NormVersion() + "\x00" + matchVersion + "\x00"))
|
||||
h.Write([]byte("gate:" + strconv.FormatBool(gateOn) + "\x00"))
|
||||
hasUnverified := false
|
||||
hasNeuter := false // a neuter row now RENDERS a directive (bank-quality §3) where it rendered nothing before
|
||||
// inScope collects the characters this fold's ROW scope actually contains, so the voice/address fold
|
||||
// below can apply the SAME scope before deciding anything (see the loop at the end).
|
||||
inScope := map[[2]string]bool{}
|
||||
|
|
@ -406,6 +414,9 @@ func ComputeVersionScopedIn(in BankInput, gateOn, excludeMined bool) string {
|
|||
if r.Status != "approved" {
|
||||
hasUnverified = true
|
||||
}
|
||||
if r.Gender == "neuter" || r.Gender == "n" {
|
||||
hasNeuter = true
|
||||
}
|
||||
inScope[[2]string{r.Src, r.Sense}] = true
|
||||
// A fixed, length-prefixed field layout so no content can forge a boundary.
|
||||
writeField(h, r.Status)
|
||||
|
|
@ -437,6 +448,13 @@ func ComputeVersionScopedIn(in BankInput, gateOn, excludeMined bool) string {
|
|||
if hasUnverified {
|
||||
h.Write([]byte("editor-unverified-section-v1\x00"))
|
||||
}
|
||||
// The same scoped-render pattern for the §3 neuter directive: gender:neuter was a SILENT no-op before,
|
||||
// so a book carrying one renders new wire bytes on the same row set — the editor-unverified hole again.
|
||||
// Folded ONLY for a scope that HAS a neuter row, so a bank without one is byte-identical to before and
|
||||
// re-bills nobody; a neuter-bearing book takes a loud, RE-PINNABLE --resnapshot (bank-only move).
|
||||
if hasNeuter {
|
||||
h.Write([]byte("neuter-directive-v1\x00"))
|
||||
}
|
||||
// The pack-19 record types, appended AFTER everything above so a book without them is byte-identical
|
||||
// to the pre-pack-19 hash by construction, not by argument.
|
||||
//
|
||||
|
|
@ -708,6 +726,9 @@ func RenderGlossaryBlock(injected []PickedEntry, tx lang.InjectionTexts) string
|
|||
// ALSO appends the confirmed-gender annotation, so a named term's gender reaches the TRANSLATOR wire, not
|
||||
// only the editor (the owner's «род должен доезжать» directive). A gendered confirmed term shifts the
|
||||
// draft injected bytes → the wire, so a loud --resnapshot; a genderless bank is byte-identical to v2.
|
||||
// (The bank-quality §3 neuter directive is NOT versioned here: a blanket bump is un-re-pinnable and would
|
||||
// re-snapshot every book on earth. It is folded SCOPED — like the editor-unverified section — so only a
|
||||
// book that actually carries a neuter row moves; see the neuter-directive tag in ComputeVersionScopedIn.)
|
||||
const RenderFormatVersion = "renderfmt-v3-draft-gender+editor-src2dst+dc3-gender"
|
||||
|
||||
// The editor's canonical-constraint block header and the glossary header are TARGET-language wire-text
|
||||
|
|
@ -818,6 +839,8 @@ func genderConstraintNote(gender string, tx lang.InjectionTexts) string {
|
|||
return tx.GenderMale
|
||||
case "female", "f":
|
||||
return tx.GenderFemale
|
||||
case "neuter", "n":
|
||||
return tx.GenderNeuter
|
||||
case "hidden":
|
||||
return tx.GenderHidden
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"strings"
|
||||
"unicode"
|
||||
|
||||
"textmachine/backend/internal/lang"
|
||||
"textmachine/backend/internal/text"
|
||||
)
|
||||
|
||||
|
|
@ -81,6 +82,7 @@ func (b *Bank) Postcheck(injected []PickedEntry, output string) PostcheckResult
|
|||
nout := text.NormalizeTargetForm(output)
|
||||
var res PostcheckResult
|
||||
noutRunes := []rune(nout)
|
||||
outWords := lang.TokenizeWords(nout) // once for the whole chunk; the §3 stem branch reuses it per term
|
||||
for _, p := range injected {
|
||||
if !p.valid() { // a caller-built zero record carries no row — nothing to check (pack-16 tail)
|
||||
continue
|
||||
|
|
@ -91,7 +93,7 @@ func (b *Bank) Postcheck(injected []PickedEntry, output string) PostcheckResult
|
|||
if strings.TrimSpace(p.entry.dst) == "" { // a ruby candidate with no dst yet — nothing to check
|
||||
continue
|
||||
}
|
||||
present := dstFormPresent(p.entry, noutRunes)
|
||||
present := dstFormPresent(p.entry, noutRunes, outWords, b.stemmer)
|
||||
if p.Disp != Confirmed {
|
||||
res.Shown++
|
||||
if present {
|
||||
|
|
@ -141,9 +143,17 @@ func (b *Bank) Postcheck(injected []PickedEntry, output string) PostcheckResult
|
|||
// The remaining inflection_gap (18/55: a plural rendered but only the singular seeded) is a
|
||||
// SEED completeness fix (Polygon), not a code one. noutRunes is the normalized output
|
||||
// pre-decomposed.
|
||||
func dstFormPresent(e *entry, noutRunes []rune) bool {
|
||||
if base := text.NormalizeTargetForm(e.dst); base != "" && containsWholeWord(noutRunes, []rune(base)) {
|
||||
return true
|
||||
func dstFormPresent(e *entry, noutRunes []rune, outWords []string, stemmer lang.TargetStemmer) bool {
|
||||
if base := text.NormalizeTargetForm(e.dst); base != "" {
|
||||
if containsWholeWord(noutRunes, []rune(base)) {
|
||||
return true
|
||||
}
|
||||
// §3: accept an OBLIQUE case of the base when the seed listed no decl forms (the measured 142/142-null
|
||||
// noise). A component-wise stem match — «горы Цинмао» for «гора Цинмао», «мечом» for «меч» — with no
|
||||
// assumption about which word inflects. Inert for a target with no decl_suffix registry.
|
||||
if declinedFormPresent(outWords, base, stemmer) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, f := range e.declForms {
|
||||
if f != "" && containsWholeWord(noutRunes, []rune(f)) {
|
||||
|
|
@ -153,6 +163,33 @@ func dstFormPresent(e *entry, noutRunes []rune) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// declinedFormPresent reports whether the base rendering appears in the tokenized output with each of its
|
||||
// words possibly inflected — a window of outWords the same length as base's words where every component
|
||||
// shares its stem. Conservative (needs the whole phrase present, in order) and inert without a stemmer.
|
||||
func declinedFormPresent(outWords []string, base string, stemmer lang.TargetStemmer) bool {
|
||||
if !stemmer.Enabled() {
|
||||
return false
|
||||
}
|
||||
bw := lang.TokenizeWords(base)
|
||||
n := len(bw)
|
||||
if n == 0 || n > len(outWords) {
|
||||
return false
|
||||
}
|
||||
for i := 0; i+n <= len(outWords); i++ {
|
||||
all := true
|
||||
for k := 0; k < n; k++ {
|
||||
if !stemmer.SameStem(outWords[i+k], bw[k]) {
|
||||
all = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if all {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// containsWholeWord reports whether form occurs in hay with a letter-boundary on both
|
||||
// ends (the char before the match is not a letter or is the start; likewise after). Runs
|
||||
// on runes so Cyrillic boundaries are correct. O(len(hay)·len(form)) — fine, the
|
||||
|
|
@ -203,7 +240,9 @@ type SpoilerLeak struct {
|
|||
//
|
||||
// Pure and deterministic (rejected order, which Select fixes). Observability only — never a disposition.
|
||||
func (b *Bank) SpoilerLeaks(rejected []PickedEntry, output string) []SpoilerLeak {
|
||||
nout := []rune(text.NormalizeTargetForm(output))
|
||||
norm := text.NormalizeTargetForm(output)
|
||||
nout := []rune(norm)
|
||||
outWords := lang.TokenizeWords(norm)
|
||||
var out []SpoilerLeak
|
||||
for _, p := range rejected {
|
||||
if !p.valid() || p.Sticky {
|
||||
|
|
@ -212,7 +251,7 @@ func (b *Bank) SpoilerLeaks(rejected []PickedEntry, output string) []SpoilerLeak
|
|||
if strings.TrimSpace(p.entry.dst) == "" {
|
||||
continue // nothing to leak
|
||||
}
|
||||
if !dstFormPresent(p.entry, nout) {
|
||||
if !dstFormPresent(p.entry, nout, outWords, b.stemmer) {
|
||||
continue
|
||||
}
|
||||
out = append(out, SpoilerLeak{
|
||||
|
|
|
|||
|
|
@ -491,9 +491,31 @@ func SeedLint(path string) error {
|
|||
if problems := ApprovedSharedKeyCollisions(entries); len(problems) > 0 {
|
||||
return fmt.Errorf("glossary seed %s shared-key collisions:\n - %s", path, strings.Join(problems, "\n - "))
|
||||
}
|
||||
if bad := GenderVocabViolations(entries); len(bad) > 0 {
|
||||
return fmt.Errorf("glossary seed %s unknown gender values (each would SILENTLY inject no gender directive — row 84):\n - %s", path, strings.Join(bad, "\n - "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// knownGenders is the gender vocabulary genderConstraintNote understands. Matched EXACTLY (no trim/case
|
||||
// fold), mirroring that switch: a value it does not recognise injects no directive at all, so " male" or
|
||||
// "Male" is as silent a no-op as a typo and is flagged the same way.
|
||||
var knownGenders = map[string]bool{
|
||||
"": true, "male": true, "m": true, "female": true, "f": true, "neuter": true, "n": true, "hidden": true,
|
||||
}
|
||||
|
||||
// GenderVocabViolations lists seed rows whose gender is outside the known vocabulary — the row-84 class where
|
||||
// gender:neuter (now valid) was an unnoticed no-op. Deterministic, seed order preserved.
|
||||
func GenderVocabViolations(entries []store.GlossaryEntry) []string {
|
||||
var out []string
|
||||
for _, e := range entries {
|
||||
if !knownGenders[e.Gender] {
|
||||
out = append(out, fmt.Sprintf("%s→%s: gender %q is not one of male|female|neuter|hidden", e.Src, e.Dst, e.Gender))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ruby classifier classes (deterministic HINTS for human promotion).
|
||||
const (
|
||||
rubyClassName = "name_candidate" // all-Han base + all-kana reading: the furigana-name SHAPE
|
||||
|
|
|
|||
|
|
@ -135,11 +135,12 @@ func (r *Runner) runBankMiningStop(ctx context.Context, chunks []chunk.Chunk, dr
|
|||
}
|
||||
return false, nil
|
||||
}
|
||||
consolidated, tres, err := r.runTerminologist(ctx, draftSnapshot, cands)
|
||||
consolidated, classified, tres, err := r.runTerminologist(ctx, draftSnapshot, cands)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
r.lastTerminology = tres
|
||||
mined = attachClassifiedType(mined, classified)
|
||||
mined = attachConsolidatedDst(mined, consolidated)
|
||||
|
||||
// Non-empty delta → write the owner signature map (the mined seed-delta YAML) and STOP before the edit wave.
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ type miningStopOpts struct {
|
|||
glossarySeed string // glossary seed YAML body; "" = no seed
|
||||
source string // book source (defaults to miningStopSource)
|
||||
terminology bool // enable the terminologist role (pack-20) with a fixture prompt
|
||||
classify bool // enable the §2 classifier phase (needs terminology) with a fixture prompt
|
||||
batchRunes int // gates.terminology.batch_runes; 0 = engine default (one batch for this corpus)
|
||||
budgetUSD float64 // gates.terminology.budget_usd; 0 = 1.0 (effectively unbounded here)
|
||||
targetScript string // gates.terminology.target_script; "" = Cyrillic (the fixtures' target)
|
||||
|
|
@ -92,6 +93,9 @@ func setupMiningStopProject(t *testing.T, providerURL string, o miningStopOpts)
|
|||
if o.batchRunes > 0 {
|
||||
gates += fmt.Sprintf(" batch_runes: %d\n", o.batchRunes)
|
||||
}
|
||||
if o.classify {
|
||||
gates += " classify_types: true\n classify_budget_usd: 1.0\n"
|
||||
}
|
||||
}
|
||||
bookPath := setupProjectOpts(t, providerURL, projectOpts{
|
||||
source: o.source,
|
||||
|
|
@ -106,6 +110,11 @@ func setupMiningStopProject(t *testing.T, providerURL string, o miningStopOpts)
|
|||
writeFile(t, filepath.Join(dir, "pairs", "zh-ru.yaml"), "pair: zh-ru\nprompts_root: ../prompts\n")
|
||||
writeFile(t, filepath.Join(dir, "prompts", "zh-ru", "terminologist.md"),
|
||||
"Ты — терминолог {{source_lang}}→{{target_lang}}.\n---USER---\nТермины книги:\n\n{{text}}")
|
||||
if o.classify {
|
||||
// A DISTINCT user header so isClassifierBody can tell a classify call from a render call on the wire.
|
||||
writeFile(t, filepath.Join(dir, "prompts", "zh-ru", "classifier.md"),
|
||||
"Ты — классификатор {{source_lang}}→{{target_lang}}.\n---USER---\nКлассы терминов:\n\n{{text}}")
|
||||
}
|
||||
}
|
||||
|
||||
packRoot, err := filepath.Abs("../../configs/langpacks")
|
||||
|
|
@ -300,6 +309,59 @@ func TestMiningStopWHATSurvivesResume(t *testing.T) {
|
|||
// candidate-block label the role prompt writes, which no other prompt does).
|
||||
func isTerminologyBody(body string) bool { return strings.Contains(body, "Термины книги") }
|
||||
|
||||
// isClassifierBody detects the §2 classifier call by its distinct fixture user header.
|
||||
func isClassifierBody(body string) bool { return strings.Contains(body, "Классы терминов") }
|
||||
|
||||
// TestClassifierCorrectsTypeBeforeRender is the §2 pin: a focused pass re-types a candidate BEFORE the
|
||||
// render, and the corrected type reaches the banked row (attachClassifiedType) and the run's counters and
|
||||
// ledger — on its OWN cost axis, separate from the render phase. Here the classifier re-types 青茅山 from the
|
||||
// draft `place` to `term`; the signature map must record the corrected type.
|
||||
func TestClassifierCorrectsTypeBeforeRender(t *testing.T) {
|
||||
rec := &reqRec{}
|
||||
var sawClassify, sawRenderAfterClassify bool
|
||||
srv := newJSONProvider(rec, func(body string) (string, string) {
|
||||
if isClassifierBody(body) {
|
||||
sawClassify = true
|
||||
return "方源\tname\n青茅山\tterm", "stop" // re-type the place to a realia term
|
||||
}
|
||||
if isTerminologyBody(body) {
|
||||
sawRenderAfterClassify = sawClassify // the render must run AFTER the classify phase
|
||||
return "方源\tФан Юань\n青茅山\tгора Цинмао", "stop"
|
||||
}
|
||||
return "Фан Юань пришёл к горе Цинмао.\n" + bankBlockForMining, "stop"
|
||||
})
|
||||
defer srv.Close()
|
||||
bookPath := setupMiningStopProject(t, srv.URL, miningStopOpts{terminology: true, classify: true})
|
||||
|
||||
r := newVerifyRunner(t, bookPath)
|
||||
defer r.Close()
|
||||
runToSignatureStop(t, r)
|
||||
yamlMap := readSignatureMap(t, r)
|
||||
|
||||
if !sawClassify {
|
||||
t.Fatal("the classifier phase must have made a call")
|
||||
}
|
||||
if !sawRenderAfterClassify {
|
||||
t.Fatal("the render phase must run AFTER the classifier phase (the corrected type primes the render)")
|
||||
}
|
||||
if r.lastTerminology.Reclassified != 1 {
|
||||
t.Fatalf("exactly one type was re-derived, got Reclassified=%d", r.lastTerminology.Reclassified)
|
||||
}
|
||||
// The corrected type reaches the banked row, not the draft `place`.
|
||||
place := termBlock(t, yamlMap, "青茅山")
|
||||
if !strings.Contains(place, "type: term") {
|
||||
t.Fatalf("the classifier's corrected type must be the banked type:\n%s", place)
|
||||
}
|
||||
// The classify spend is attributable on its OWN cost axis, apart from the render.
|
||||
cspent, err := r.Store.RoleSpentUSD("test-book", roleClassifier)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cspent <= 0 || r.lastTerminology.ClassifyCostUSD <= 0 {
|
||||
t.Fatalf("classify spend must be on its own ledger axis: ledger=%v result=%v", cspent, r.lastTerminology.ClassifyCostUSD)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTerminologistConsolidatesBankIntoDraftRows is the core pack-20 pin: with the role on, the bank the
|
||||
// owner meets at the stop carries a CONSOLIDATED rendering per term, emitted in the `draft` mode of
|
||||
// §C2-7 — the mode that reaches the wire with ⟨проверить⟩ — while a term the role declines stays `auto`,
|
||||
|
|
|
|||
|
|
@ -70,6 +70,9 @@ type Runner struct {
|
|||
// nil otherwise, which is what makes the whole role a no-op for every book that does not enable it. It is
|
||||
// NOT snapshot-folded: the role writes a signature map, not a checkpoint (config.TerminologyGate).
|
||||
terminologyTemplate *PromptTemplate
|
||||
// classifierTemplate is the pair's §2 type-classifier prompt, loaded only when classify_types is on. nil
|
||||
// otherwise → the classifier phase is a no-op and the draft heuristic type stands.
|
||||
classifierTemplate *PromptTemplate
|
||||
// targetScript is the target language's Unicode script, resolved once from gates.terminology
|
||||
// .target_script (config validates the name). nil when the book declares none — the answer-language
|
||||
// screen is then inert, which loadTargetScript says out loud if any channel could have used it.
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"sort"
|
||||
"strings"
|
||||
"textmachine/backend/internal/chunk"
|
||||
"textmachine/backend/internal/lang"
|
||||
"textmachine/backend/internal/membank"
|
||||
"textmachine/backend/internal/store"
|
||||
)
|
||||
|
|
@ -131,7 +132,10 @@ func (r *Runner) seedGlossary(ctx context.Context) error {
|
|||
// holds the injection conditional until the polygon experiment. It is the fold's condition, so while
|
||||
// it is false a book with voice rows hashes exactly as it did without them and nobody re-pays for
|
||||
// authoring a profile. Wiring the injection means setting it and accepting a full --resnapshot.
|
||||
bankIn := membank.BankInput{Rows: rows, Voices: storedVoices, Pairs: storedPairs}
|
||||
// The §3 decl stemmer is target data, constant per book; baseIn copies bankIn below, so both banks share
|
||||
// it. A target with no decl_suffix registry yields an inert stemmer → exact-match post-check as before.
|
||||
bankIn := membank.BankInput{Rows: rows, Voices: storedVoices, Pairs: storedPairs,
|
||||
TargetStemmer: lang.NewTargetStemmer(lang.TargetChecksFor(r.Book.TargetLang))}
|
||||
r.memory = membank.MaterializeBank(bankIn, r.Pipeline.Gates.Glossary.PostcheckGate)
|
||||
// The DRAFT wave selects over a BASE-scoped bank (Source:mined excluded) so its injection is
|
||||
// byte-identical across a bank-mining enrichment — matching the draft-wave snapshot (baseMemoryVersion),
|
||||
|
|
|
|||
|
|
@ -45,6 +45,11 @@ import (
|
|||
// answerable with no migration.
|
||||
const roleTerminologist = "terminologist"
|
||||
|
||||
// roleClassifier is the §2 type-classifier phase's cost axis. Its own role marker keeps its checkpoints,
|
||||
// request_log rows and ledger entries on a queryable class of their own and on their own request-hash axis,
|
||||
// separate from the render phase — "what did classification spend" is answerable with no migration.
|
||||
const roleClassifier = "classifier"
|
||||
|
||||
// terminologyStageName is the synthetic stage name the calls carry. It is NOT a pipeline stage (a stage
|
||||
// would join a wave, take a snapshot axis and a chunk_status row per unit); it is an addressing label.
|
||||
const terminologyStageName = "terminology"
|
||||
|
|
@ -52,7 +57,7 @@ const terminologyStageName = "terminology"
|
|||
// terminologyVersion versions the ASSEMBLY algorithm (merge → KWIC → §C2-3 ranking → batching → parse). It
|
||||
// is deliberately NOT snapshot-folded — see config.TerminologyGate — but it is logged with the run so a
|
||||
// signature map can be attributed to the algorithm that produced it.
|
||||
const terminologyVersion = "terminology-v1-merge+kwic+c2-3"
|
||||
const terminologyVersion = "terminology-v2-merge+kwic+c2-3+series"
|
||||
|
||||
// Engine defaults for the block sizing. They bound ONE call's input; the whole book is covered by batching.
|
||||
const (
|
||||
|
|
@ -78,10 +83,14 @@ type terminologyResult struct {
|
|||
// OBSERVABILITY, never a gate: the rows stay unverified either way, and this is what tells the owner
|
||||
// which of them to look at first.
|
||||
CanonConflicts int
|
||||
CostUSD float64 // what THIS run paid
|
||||
CumUSD float64 // what the calls cost in total (a replayed checkpoint is $0 now, not then)
|
||||
CostUSD float64 // what THIS run's RENDER phase paid
|
||||
CumUSD float64 // what the render calls cost in total (a replayed checkpoint is $0 now, not then)
|
||||
Fresh bool // at least one call actually reached the provider this run
|
||||
EstimateUSD float64 // the pre-call projection, logged before any money moves
|
||||
// Reclassified is how many candidate types the §2 classifier phase actually changed; ClassifyCostUSD is
|
||||
// what that phase paid this run. Both zero when classify_types is off.
|
||||
Reclassified int
|
||||
ClassifyCostUSD float64
|
||||
}
|
||||
|
||||
// loadTerminologyTemplate loads the pair's terminologist prompt when the gate is on. Mirrors
|
||||
|
|
@ -95,6 +104,21 @@ func (r *Runner) loadTerminologyTemplate() error {
|
|||
return err
|
||||
}
|
||||
r.terminologyTemplate = tpl
|
||||
return r.loadClassifierTemplate()
|
||||
}
|
||||
|
||||
// loadClassifierTemplate loads the pair's §2 classifier prompt when classify_types is on. Off → nil, and the
|
||||
// classifier phase is inert. Called from loadTerminologyTemplate: the classifier only exists as a phase of
|
||||
// the terminology gate.
|
||||
func (r *Runner) loadClassifierTemplate() error {
|
||||
if !r.Pipeline.Gates.Terminology.ClassifyTypes {
|
||||
return nil
|
||||
}
|
||||
tpl, err := LoadPromptTemplate(r.Pipeline.Gates.Terminology.ClassifyPromptPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.classifierTemplate = tpl
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -159,6 +183,18 @@ func (r *Runner) buildBankCandidates(mined []miner.Term, observed []terminology.
|
|||
_, kwicPer, kwicWidth := r.terminologyOpts()
|
||||
cands = terminology.AttachKWIC(cands, chunks, kwicPer, kwicWidth)
|
||||
|
||||
opts := r.scoreOpts()
|
||||
for i := range cands {
|
||||
terminology.ScoreVariants(&cands[i], opts)
|
||||
}
|
||||
return cands
|
||||
}
|
||||
|
||||
// scoreOpts builds the §C2-3 scoring options — the approved-neighbour anchor and the pair's transliteration
|
||||
// conformance, which routes on TYPE (only name/place answer to the transliteration convention). Extracted so
|
||||
// the initial candidate build and the §2 post-classify re-score share ONE definition and can never disagree
|
||||
// about how a variant is scored.
|
||||
func (r *Runner) scoreOpts() terminology.ScoreOpts {
|
||||
opts := terminology.ScoreOpts{Neighbours: r.approvedNeighbours()}
|
||||
if r.pack != nil {
|
||||
pack := r.pack
|
||||
|
|
@ -169,10 +205,7 @@ func (r *Runner) buildBankCandidates(mined []miner.Term, observed []terminology.
|
|||
return miner.PalladiusConformance(dst, pack)
|
||||
}
|
||||
}
|
||||
for i := range cands {
|
||||
terminology.ScoreVariants(&cands[i], opts)
|
||||
}
|
||||
return cands
|
||||
return opts
|
||||
}
|
||||
|
||||
// approvedNeighbours is the already-signed bank, as the §C2-3 "agreement with approved siblings" anchor.
|
||||
|
|
@ -199,10 +232,10 @@ func (r *Runner) approvedNeighbours() []terminology.Neighbour {
|
|||
// A term absent from the returned map keeps NO dst — the auto branch of §C2-7. That is the deliberate
|
||||
// reading of silence: a role that did not answer has not decided, and an undecided term must not enter the
|
||||
// wire carrying a guess.
|
||||
func (r *Runner) runTerminologist(ctx context.Context, snapID string, cands []terminology.Candidate) (map[string]string, terminologyResult, error) {
|
||||
func (r *Runner) runTerminologist(ctx context.Context, snapID string, cands []terminology.Candidate) (map[string]string, map[string]string, terminologyResult, error) {
|
||||
var res terminologyResult
|
||||
if !r.Pipeline.Gates.Terminology.Enabled || r.terminologyTemplate == nil || len(cands) == 0 {
|
||||
return nil, res, nil
|
||||
return nil, nil, res, nil
|
||||
}
|
||||
res.Candidates = len(cands)
|
||||
for _, c := range cands {
|
||||
|
|
@ -211,96 +244,65 @@ func (r *Runner) runTerminologist(ctx context.Context, snapID string, cands []te
|
|||
}
|
||||
}
|
||||
batchRunes, _, _ := r.terminologyOpts()
|
||||
batches := terminology.Batch(cands, batchRunes)
|
||||
|
||||
// §2 type re-derivation, BEFORE the render: a focused pass re-classifies every candidate, so a realia
|
||||
// surface mistyped as a name (元石) is no longer FORCED to a transliteration. The corrected type routes
|
||||
// conformance (re-scored here), primes the wire block, and is returned so the caller stamps it as the
|
||||
// banked type. Off (or unanswered) → the heuristic draft type stands.
|
||||
classified, crun, cerr := r.runClassifier(ctx, snapID, cands)
|
||||
if cerr != nil {
|
||||
return nil, nil, res, cerr
|
||||
}
|
||||
res.ClassifyCostUSD = crun.costUSD
|
||||
if len(classified) > 0 {
|
||||
res.Reclassified = applyTypes(cands, classified)
|
||||
opts := r.scoreOpts()
|
||||
for i := range cands {
|
||||
terminology.ScoreVariants(&cands[i], opts)
|
||||
}
|
||||
}
|
||||
|
||||
// §1: co-batch grade/rank series so the role picks ONE generic head for the whole set. The pair-data —
|
||||
// whether the source forms rune-morpheme series and where the head sits — comes from the language layer,
|
||||
// so the batcher stays pair-agnostic and an alphabetic source takes a byte-identical, series-free path.
|
||||
sEnabled, headFinal := lang.SeriesMorphology(r.Book.SourceLang)
|
||||
seriesID := terminology.DetectSeries(cands, terminology.SeriesParams{Enabled: sEnabled, HeadFinal: headFinal})
|
||||
batches := terminology.Batch(cands, batchRunes, seriesID)
|
||||
res.Batches = len(batches)
|
||||
|
||||
// The ESTIMATE comes before any money moves — the directive's «смета ДО живого вызова». It is the same
|
||||
// arithmetic the reservation uses, summed over the batches, so the number the operator reads and the
|
||||
// number the ledger reserves cannot drift apart.
|
||||
// The signed bank is read ONCE for the whole role, not per batch: it is the same law for every batch and
|
||||
// a store read per batch would be a query multiplied by a number the operator does not control.
|
||||
// The signed bank is read ONCE for the whole role, not per batch: it is the same law for every batch.
|
||||
canon := r.approvedNeighbours()
|
||||
msgsPer := make([][]llm.Message, len(batches))
|
||||
for i, b := range batches {
|
||||
m, err := r.terminologyMessages(b, canon)
|
||||
if err != nil {
|
||||
return nil, res, err
|
||||
}
|
||||
msgsPer[i] = m
|
||||
res.EstimateUSD += r.terminologyEstimateUSD(m)
|
||||
plan := bankRolePlan{
|
||||
role: roleTerminologist, model: r.Pipeline.Gates.Terminology.Model, budgetUSD: r.Pipeline.Gates.Terminology.BudgetUSD,
|
||||
messages: func(b []terminology.Candidate) ([]llm.Message, error) { return r.terminologyMessages(b, canon) },
|
||||
}
|
||||
r.Log.InfoContext(ctx, "terminology: consolidating the bank over the whole book",
|
||||
"book", r.Book.BookID, "candidates", res.Candidates, "reverse_section", res.Reverse,
|
||||
"batches", res.Batches, "model", r.Pipeline.Gates.Terminology.Model,
|
||||
"estimate_usd", fmt.Sprintf("%.6f", res.EstimateUSD), "budget_usd", r.Pipeline.Gates.Terminology.BudgetUSD,
|
||||
"version", terminologyVersion)
|
||||
run, err := r.runBankRoleBatches(ctx, snapID, plan, batches, "render")
|
||||
if err != nil {
|
||||
return nil, nil, res, err
|
||||
}
|
||||
res.EstimateUSD, res.CostUSD, res.CumUSD, res.Fresh = run.estimateUSD, run.costUSD, run.cumUSD, run.fresh
|
||||
|
||||
spent, err := r.Store.RoleSpentUSD(r.Book.BookID, roleTerminologist)
|
||||
if err != nil {
|
||||
return nil, res, fmt.Errorf("pipeline: terminology budget read: %w", err)
|
||||
}
|
||||
out := map[string]string{}
|
||||
st := config.Stage{Name: terminologyStageName, Role: roleTerminologist, Model: r.Pipeline.Gates.Terminology.Model}
|
||||
job, err := r.Store.EnsureJob(r.Book.BookID, 0, terminologyStageName, snapID)
|
||||
if err != nil {
|
||||
return nil, res, fmt.Errorf("pipeline: terminology job: %w", err)
|
||||
}
|
||||
for i, b := range batches {
|
||||
// The synthetic chunk addresses the batch: chapter 0 is the BOOK level (no real chapter is 0), and
|
||||
// the batch ordinal is the chunk index, so two batches can never collide on one checkpoint.
|
||||
ch := chunk.Chunk{Chapter: 0, ChunkIdx: i}
|
||||
paid, perr := r.terminologyCheckpointExists(st, snapID, ch, msgsPer[i])
|
||||
if perr != nil {
|
||||
return nil, res, perr
|
||||
if run.texts[i] == "" {
|
||||
continue
|
||||
}
|
||||
// The budget is checked BEFORE the call and against what the call would COST, not after the fact
|
||||
// against what has been spent: a check on spend alone lets the last permitted call push the total
|
||||
// past the ceiling by its own size, so the number in the config is not the bound it looks like. The
|
||||
// projection is the reservation's own upper bound (it sizes by max_tokens), so this refuses on the
|
||||
// conservative side — it can decline a call that would have fitted, never permit one that does not.
|
||||
if want := r.terminologyEstimateUSD(msgsPer[i]); !paid && spent+want > r.Pipeline.Gates.Terminology.BudgetUSD {
|
||||
r.Log.WarnContext(ctx, "terminology budget would be exceeded by the next batch; the remaining terms stay unconsolidated (status:auto)",
|
||||
"book", r.Book.BookID, "spent_usd", fmt.Sprintf("%.6f", spent), "next_batch_usd", fmt.Sprintf("%.6f", want),
|
||||
"budget_usd", r.Pipeline.Gates.Terminology.BudgetUSD, "batches_left", len(batches)-i)
|
||||
break
|
||||
}
|
||||
att, aerr := r.runTerminologyAttempt(ctx, st, snapID, ch, job, msgsPer[i])
|
||||
if aerr != nil {
|
||||
// A ceiling denial must not abort the book: this step is optional and the draft wave is already
|
||||
// paid for. Degrade to "no consolidation" exactly as the repair sub-step degrades.
|
||||
if errors.Is(aerr, errReserveCeiling) {
|
||||
r.Log.WarnContext(ctx, "terminology call denied by a USD ceiling; the bank stays unconsolidated", "book", r.Book.BookID)
|
||||
break
|
||||
}
|
||||
return nil, res, aerr
|
||||
}
|
||||
res.CostUSD += att.runCost
|
||||
res.CumUSD += att.cumCost
|
||||
res.Fresh = res.Fresh || att.freshCall
|
||||
spent += att.runCost
|
||||
keys := make([]string, 0, len(b))
|
||||
for _, c := range b {
|
||||
keys = append(keys, c.Key)
|
||||
}
|
||||
got, st := terminology.ParseReply(att.text, keys, text.NormalizeSourceKey, r.targetScript)
|
||||
got, st := terminology.ParseReply(run.texts[i], candKeys(b), text.NormalizeSourceKey, r.targetScript)
|
||||
res.BadLines += st.Bad
|
||||
res.OffLanguage += st.OffLanguage
|
||||
// A batch answered largely in another language is the measured cold-start failure, not noise: say
|
||||
// it while the run is happening, naming the lines, because the terms themselves just stay auto.
|
||||
// A batch answered largely in another language is the measured cold-start failure, not noise: say it
|
||||
// while the run is happening, naming the lines, because the terms themselves just stay auto.
|
||||
if st.OffLanguage > 0 {
|
||||
r.Log.WarnContext(ctx, "terminology: the model answered in another script; those lines are REFUSED (the terms stay unconsolidated) — a book with few signed rows gives the model no target-language anchor",
|
||||
"book", r.Book.BookID, "batch", i, "terms", len(b), "off_language", st.OffLanguage,
|
||||
"target_script", r.Pipeline.Gates.Terminology.TargetScript, "lines", strings.Join(st.OffLanguageSamples, "; "))
|
||||
}
|
||||
// A batch that came back with NOTHING usable is a paid call that bought no terminology: an empty
|
||||
// completion, a truncation, a refusal, a reply in prose. Silence here would spend the money, mark
|
||||
// every term of the batch "the role declined" (§C2-7's auto mode, which is a DECISION) and exit 0.
|
||||
// Say it out loud instead — the run still continues, because an unconsolidated bank is the state
|
||||
// this step started from, not a broken one.
|
||||
// A batch that came back with NOTHING usable is a paid call that bought no terminology. Silence here
|
||||
// would spend the money, mark every term "the role declined" and exit 0. Say it out loud; the run still
|
||||
// continues, because an unconsolidated bank is the state this step started from, not a broken one.
|
||||
if len(got) == 0 {
|
||||
r.Log.WarnContext(ctx, "terminology: a paid batch returned nothing the parser could use; its terms stay unconsolidated",
|
||||
"book", r.Book.BookID, "batch", i, "terms", len(b), "bad_lines", st.Bad,
|
||||
"reply_chars", len(att.text), "cost_usd", fmt.Sprintf("%.6f", att.runCost))
|
||||
"book", r.Book.BookID, "batch", i, "terms", len(b), "bad_lines", st.Bad, "reply_chars", len(run.texts[i]))
|
||||
}
|
||||
for k, v := range got {
|
||||
out[k] = v
|
||||
|
|
@ -329,11 +331,24 @@ func (r *Runner) runTerminologist(ctx context.Context, snapID string, cands []te
|
|||
r.Log.WarnContext(ctx, "terminology: consolidated renderings contradict the SIGNED bank; they stay unverified (⟨проверить⟩) and are the first rows to review at the stop",
|
||||
"book", r.Book.BookID, "conflicts", len(conflicts), "terms", strings.Join(named, "; "))
|
||||
}
|
||||
// The $0 label screen (§2 warm-run hygiene): a name/place row whose rendering was clearly TRANSLATED is a
|
||||
// label/rendering disagreement worth a human's eye. It is a review FLAG, never a gate, and explicitly not
|
||||
// a safety net for the transliteration harm — the classifier phase is what prevents that (see
|
||||
// terminology.TypeLabelMismatches). Named, not a bare count, so the operator knows which rows to open.
|
||||
if flags := terminology.TypeLabelMismatches(labelRows(cands, out)); len(flags) > 0 {
|
||||
named := make([]string, 0, len(flags))
|
||||
for _, f := range flags {
|
||||
named = append(named, fmt.Sprintf("%s [%s]→%q", f.Src, f.Type, f.Dst))
|
||||
}
|
||||
r.Log.WarnContext(ctx, "terminology: name/place rows carry a translated rendering — a label/rendering mismatch to review (hygiene flag, not a gate)",
|
||||
"book", r.Book.BookID, "rows", len(flags), "terms", strings.Join(named, "; "))
|
||||
}
|
||||
r.Log.InfoContext(ctx, "terminology finished", "book", r.Book.BookID,
|
||||
"consolidated", res.Consolidated, "declined", res.Declined, "unanswered", res.Unanswered,
|
||||
"bad_lines", res.BadLines, "off_language", res.OffLanguage,
|
||||
"canon_conflicts", res.CanonConflicts, "cost_usd", fmt.Sprintf("%.6f", res.CostUSD))
|
||||
return out, res, nil
|
||||
"reclassified", res.Reclassified, "bad_lines", res.BadLines, "off_language", res.OffLanguage,
|
||||
"canon_conflicts", res.CanonConflicts, "cost_usd", fmt.Sprintf("%.6f", res.CostUSD),
|
||||
"classify_cost_usd", fmt.Sprintf("%.6f", res.ClassifyCostUSD))
|
||||
return out, classified, res, nil
|
||||
}
|
||||
|
||||
// terminologyMessages renders ONE batch into the wire messages: the pair's authored role prompt (system),
|
||||
|
|
@ -356,10 +371,87 @@ func (r *Runner) terminologyMessages(batch []terminology.Candidate, canon []term
|
|||
// cost more than the candidates it accompanies.
|
||||
const terminologyCanonCap = 40
|
||||
|
||||
// terminologyCallBudget resolves the model and max_tokens of a terminology call — ONE definition, so the
|
||||
// checkpoint probe and the call itself can never address different request hashes (the repair precedent).
|
||||
func (r *Runner) terminologyCallBudget(msgs []llm.Message) (string, int) {
|
||||
model := r.Pipeline.Gates.Terminology.Model
|
||||
// runClassifier is the §2 type-classifier phase: a focused pass on the same candidate batches that returns
|
||||
// key → corrected type. Off (or no template) → a nil map and a zero run, so the terminologist keeps the
|
||||
// draft heuristic type and pays nothing. The classifier needs no series co-batching (it decides a class per
|
||||
// term) and no canon anchor (a class is a property of the source, not of the signed bank).
|
||||
func (r *Runner) runClassifier(ctx context.Context, snapID string, cands []terminology.Candidate) (map[string]string, bankRoleRun, error) {
|
||||
g := r.Pipeline.Gates.Terminology
|
||||
if !g.ClassifyTypes || r.classifierTemplate == nil || len(cands) == 0 {
|
||||
return nil, bankRoleRun{}, nil
|
||||
}
|
||||
batchRunes, _, _ := r.terminologyOpts()
|
||||
batches := terminology.Batch(cands, batchRunes, nil)
|
||||
plan := bankRolePlan{role: roleClassifier, model: g.ClassifierModel(), budgetUSD: g.ClassifyBudgetUSD, messages: r.classifierMessages}
|
||||
run, err := r.runBankRoleBatches(ctx, snapID, plan, batches, "classify")
|
||||
if err != nil {
|
||||
return nil, run, err
|
||||
}
|
||||
out := map[string]string{}
|
||||
bad := 0
|
||||
for i, b := range batches {
|
||||
if run.texts[i] == "" {
|
||||
continue
|
||||
}
|
||||
got, st := terminology.ParseTypes(run.texts[i], candKeys(b), text.NormalizeSourceKey)
|
||||
bad += st.Bad
|
||||
for k, v := range got {
|
||||
out[k] = v
|
||||
}
|
||||
}
|
||||
if bad > 0 {
|
||||
r.Log.WarnContext(ctx, "terminology classify: some reply lines were off-vocabulary or malformed; those terms keep their draft type",
|
||||
"book", r.Book.BookID, "bad_lines", bad)
|
||||
}
|
||||
return out, run, nil
|
||||
}
|
||||
|
||||
// classifierMessages renders ONE batch into the classifier's wire messages: the pair's authored classifier
|
||||
// prompt (system) and the candidate block as the user turn, with no canon anchor. Engine-neutral, like the
|
||||
// terminologist block — the class DEFINITIONS live in the pair's prompt, so a new pair needs no Go edit.
|
||||
func (r *Runner) classifierMessages(batch []terminology.Candidate) ([]llm.Message, error) {
|
||||
return MessagesWithInjection(r.classifierTemplate,
|
||||
RenderVars{Book: r.Book, Text: terminology.RenderBatch(batch)}, "")
|
||||
}
|
||||
|
||||
// applyTypes stamps the classifier's corrected types onto the candidates in place and returns how many it
|
||||
// actually changed. A term the classifier did not answer keeps its draft type.
|
||||
func applyTypes(cands []terminology.Candidate, classified map[string]string) int {
|
||||
n := 0
|
||||
for i := range cands {
|
||||
if t, ok := classified[cands[i].Key]; ok && t != "" && t != cands[i].Type {
|
||||
cands[i].Type = t
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// candKeys is the batch's candidate keys, in order — what the reply parsers screen a reply against.
|
||||
func candKeys(cands []terminology.Candidate) []string {
|
||||
keys := make([]string, len(cands))
|
||||
for i, c := range cands {
|
||||
keys[i] = c.Key
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// labelRows pairs each candidate that received a consolidated rendering with its (corrected) type, for the
|
||||
// $0 label screen.
|
||||
func labelRows(cands []terminology.Candidate, consolidated map[string]string) []terminology.LabelRow {
|
||||
var rows []terminology.LabelRow
|
||||
for _, c := range cands {
|
||||
if dst := consolidated[c.Key]; dst != "" {
|
||||
rows = append(rows, terminology.LabelRow{Src: c.Src, Type: c.Type, Dst: dst})
|
||||
}
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
// bankCallBudget resolves the model and max_tokens of a bank-role call (terminologist OR classifier) — ONE
|
||||
// definition, so the checkpoint probe and the call itself can never address different request hashes (the
|
||||
// repair precedent).
|
||||
func (r *Runner) bankCallBudget(model string, msgs []llm.Message) (string, int) {
|
||||
est := 0
|
||||
for _, m := range msgs {
|
||||
est += EstimateTokens(m.Content)
|
||||
|
|
@ -377,10 +469,10 @@ func (r *Runner) terminologyCallBudget(msgs []llm.Message) (string, int) {
|
|||
// terminologyReplyFloor is the headroom one batch's reply needs beyond the proportional estimate.
|
||||
const terminologyReplyFloor = 256
|
||||
|
||||
// terminologyEstimateUSD projects ONE call's cost with the same price/estimate arithmetic the reservation
|
||||
// uses, so the pre-call number and the reserved number are the same number.
|
||||
func (r *Runner) terminologyEstimateUSD(msgs []llm.Message) float64 {
|
||||
model, maxTokens := r.terminologyCallBudget(msgs)
|
||||
// bankCallEstimateUSD projects ONE call's cost with the same price/estimate arithmetic the reservation uses,
|
||||
// so the pre-call number and the reserved number are the same number.
|
||||
func (r *Runner) bankCallEstimateUSD(model string, msgs []llm.Message) float64 {
|
||||
_, maxTokens := r.bankCallBudget(model, msgs)
|
||||
promptEst := 0
|
||||
for _, m := range msgs {
|
||||
promptEst += EstimateTokens(m.Content)
|
||||
|
|
@ -389,37 +481,120 @@ func (r *Runner) terminologyEstimateUSD(msgs []llm.Message) float64 {
|
|||
r.Models.AdditiveReasoningTokens(model, "", 0))
|
||||
}
|
||||
|
||||
// terminologyCheckpointExists reports whether THIS batch was already paid for in an earlier run.
|
||||
func (r *Runner) terminologyCheckpointExists(st config.Stage, snapID string, ch chunk.Chunk, msgs []llm.Message) (bool, error) {
|
||||
model, maxTokens := r.terminologyCallBudget(msgs)
|
||||
// bankCheckpointExists reports whether THIS batch was already paid for in an earlier run, on the role's own
|
||||
// request-hash axis.
|
||||
func (r *Runner) bankCheckpointExists(role, model string, st config.Stage, snapID string, ch chunk.Chunk, msgs []llm.Message) (bool, error) {
|
||||
_, maxTokens := r.bankCallBudget(model, msgs)
|
||||
cp, err := r.Store.GetCheckpoint(RequestHash(Request{
|
||||
BookID: r.Book.BookID, Chapter: ch.Chapter, ChunkIdx: ch.ChunkIdx, Attempt: 0,
|
||||
Stage: st.Name, Role: roleTerminologist, Model: model, MaxTokens: maxTokens, SnapshotID: snapID, Messages: msgs,
|
||||
Stage: st.Name, Role: role, Model: model, MaxTokens: maxTokens, SnapshotID: snapID, Messages: msgs,
|
||||
}))
|
||||
return cp != nil, err
|
||||
}
|
||||
|
||||
// runTerminologyAttempt performs ONE terminology call on the shared money path: reserve → call →
|
||||
// settle+checkpoint, with a checkpoint hit replayed for free. It reuses runAttempt rather than
|
||||
// re-implementing the money sequence — a second copy of that sequence is the drift this codebase already
|
||||
// paid to remove once. isFinal=false: the reply is a term table, not shipping prose, so the output
|
||||
// sanitizer must not judge it; the intrinsic classifier still runs and is a real guard (a refusal reply
|
||||
// would otherwise be parsed as terminology).
|
||||
func (r *Runner) runTerminologyAttempt(ctx context.Context, st config.Stage, snapID string, ch chunk.Chunk,
|
||||
// runBankAttempt performs ONE bank-role call on the shared money path: reserve → call → settle+checkpoint,
|
||||
// with a checkpoint hit replayed for free. It reuses runAttempt rather than re-implementing the money
|
||||
// sequence — a second copy of that sequence is the drift this codebase already paid to remove once.
|
||||
// isFinal=false: the reply is a term table, not shipping prose, so the output sanitizer must not judge it;
|
||||
// the intrinsic classifier still runs and is a real guard (a refusal reply would otherwise be parsed as
|
||||
// terminology).
|
||||
func (r *Runner) runBankAttempt(ctx context.Context, role, model string, st config.Stage, snapID string, ch chunk.Chunk,
|
||||
job *store.Job, msgs []llm.Message) (stageAttempt, error) {
|
||||
|
||||
model, maxTokens := r.terminologyCallBudget(msgs)
|
||||
tst := config.Stage{Name: st.Name, Role: roleTerminologist, Model: model}
|
||||
// The log axis, exactly as runStage sets it for a chunk stage: without it every terminology line in a
|
||||
// paid run reads "calling model deepseek-v4-flash" with no book, no role and no batch — unattributable
|
||||
// in a multi-book log and unmatchable to the batch that produced a bad reply. Chunk carries the batch
|
||||
// ordinal, which is what ch already addresses.
|
||||
_, maxTokens := r.bankCallBudget(model, msgs)
|
||||
tst := config.Stage{Name: st.Name, Role: role, Model: model}
|
||||
// The log axis, exactly as runStage sets it for a chunk stage: without it every bank-role line in a paid
|
||||
// run reads "calling model deepseek-v4-flash" with no book, no role and no batch — unattributable in a
|
||||
// multi-book log and unmatchable to the batch that produced a bad reply. Chunk carries the batch ordinal.
|
||||
ri, _ := obs.ReqInfoFromContext(ctx)
|
||||
ri.Book, ri.Chapter, ri.Chunk, ri.Stage, ri.Role = r.Book.BookID, ch.Chapter, ch.ChunkIdx, tst.Name, roleTerminologist
|
||||
ri.Book, ri.Chapter, ri.Chunk, ri.Stage, ri.Role = r.Book.BookID, ch.Chapter, ch.ChunkIdx, tst.Name, role
|
||||
ctx = obs.WithReqInfo(ctx, ri)
|
||||
return r.runAttempt(ctx, tst, model, snapID, ch, job, 0, maxTokens, msgs, false, false)
|
||||
}
|
||||
|
||||
// bankRolePlan is one bank-level role's plan over a batch list: its cost axis (role/model), its own book-wide
|
||||
// ceiling, and how it builds one batch's wire messages. The money sequence — estimate, budget-gate,
|
||||
// checkpoint-or-call — is identical for the classifier and the terminologist; only the messages and the reply
|
||||
// PARSING differ, and parsing is the caller's job on the returned texts.
|
||||
type bankRolePlan struct {
|
||||
role string
|
||||
model string
|
||||
budgetUSD float64
|
||||
messages func(batch []terminology.Candidate) ([]llm.Message, error)
|
||||
}
|
||||
|
||||
// bankRoleRun is what one role's pass produced: each batch's reply text (in batch order, "" for a batch the
|
||||
// budget cut or that failed soft), plus the cost accounting for the report.
|
||||
type bankRoleRun struct {
|
||||
texts []string
|
||||
estimateUSD float64
|
||||
costUSD float64
|
||||
cumUSD float64
|
||||
fresh bool
|
||||
}
|
||||
|
||||
// runBankRoleBatches runs plan over batches on the shared money path. The estimate is logged BEFORE any call;
|
||||
// a budget ceiling or a soft denial stops the pass and leaves the remaining terms untouched — the run never
|
||||
// aborts on this optional step. logKind names the phase (render|classify) in the logs.
|
||||
func (r *Runner) runBankRoleBatches(ctx context.Context, snapID string, plan bankRolePlan, batches [][]terminology.Candidate, logKind string) (bankRoleRun, error) {
|
||||
run := bankRoleRun{texts: make([]string, len(batches))}
|
||||
msgsPer := make([][]llm.Message, len(batches))
|
||||
for i, b := range batches {
|
||||
m, err := plan.messages(b)
|
||||
if err != nil {
|
||||
return run, err
|
||||
}
|
||||
msgsPer[i] = m
|
||||
run.estimateUSD += r.bankCallEstimateUSD(plan.model, m)
|
||||
}
|
||||
r.Log.InfoContext(ctx, "terminology "+logKind+": estimate before any call",
|
||||
"book", r.Book.BookID, "role", plan.role, "batches", len(batches), "model", plan.model,
|
||||
"estimate_usd", fmt.Sprintf("%.6f", run.estimateUSD), "budget_usd", plan.budgetUSD, "version", terminologyVersion)
|
||||
|
||||
spent, err := r.Store.RoleSpentUSD(r.Book.BookID, plan.role)
|
||||
if err != nil {
|
||||
return run, fmt.Errorf("pipeline: %s budget read: %w", plan.role, err)
|
||||
}
|
||||
st := config.Stage{Name: terminologyStageName, Role: plan.role, Model: plan.model}
|
||||
job, err := r.Store.EnsureJob(r.Book.BookID, 0, terminologyStageName, snapID)
|
||||
if err != nil {
|
||||
return run, fmt.Errorf("pipeline: %s job: %w", plan.role, err)
|
||||
}
|
||||
for i := range batches {
|
||||
// The synthetic chunk addresses the batch: chapter 0 is the BOOK level (no real chapter is 0), and the
|
||||
// batch ordinal is the chunk index, so two batches can never collide on one checkpoint.
|
||||
ch := chunk.Chunk{Chapter: 0, ChunkIdx: i}
|
||||
paid, perr := r.bankCheckpointExists(plan.role, plan.model, st, snapID, ch, msgsPer[i])
|
||||
if perr != nil {
|
||||
return run, perr
|
||||
}
|
||||
// The budget is checked BEFORE the call and against what the call would COST (the reservation's own
|
||||
// upper bound), so it refuses on the conservative side and the config number is the bound it looks like.
|
||||
if want := r.bankCallEstimateUSD(plan.model, msgsPer[i]); !paid && spent+want > plan.budgetUSD {
|
||||
r.Log.WarnContext(ctx, "terminology "+logKind+": budget would be exceeded by the next batch; the remaining terms are left unchanged",
|
||||
"book", r.Book.BookID, "role", plan.role, "spent_usd", fmt.Sprintf("%.6f", spent),
|
||||
"next_batch_usd", fmt.Sprintf("%.6f", want), "budget_usd", plan.budgetUSD, "batches_left", len(batches)-i)
|
||||
break
|
||||
}
|
||||
att, aerr := r.runBankAttempt(ctx, plan.role, plan.model, st, snapID, ch, job, msgsPer[i])
|
||||
if aerr != nil {
|
||||
// A ceiling denial must not abort the book: this step is optional and the draft wave is already
|
||||
// paid for. Degrade to "no change" exactly as the repair sub-step degrades.
|
||||
if errors.Is(aerr, errReserveCeiling) {
|
||||
r.Log.WarnContext(ctx, "terminology "+logKind+": call denied by a USD ceiling; remaining terms left unchanged", "book", r.Book.BookID, "role", plan.role)
|
||||
break
|
||||
}
|
||||
return run, aerr
|
||||
}
|
||||
run.costUSD += att.runCost
|
||||
run.cumUSD += att.cumCost
|
||||
run.fresh = run.fresh || att.freshCall
|
||||
spent += att.runCost
|
||||
run.texts[i] = att.text
|
||||
}
|
||||
return run, nil
|
||||
}
|
||||
|
||||
// attachConsolidatedDst stamps the terminologist's renderings onto the mined terms, which is what selects
|
||||
// the emission MODE in miner.DeltaYAML (§C2-7). Terms the role did not answer are left untouched.
|
||||
func attachConsolidatedDst(mined []miner.Term, consolidated map[string]string) []miner.Term {
|
||||
|
|
@ -435,3 +610,20 @@ func attachConsolidatedDst(mined []miner.Term, consolidated map[string]string) [
|
|||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// attachClassifiedType stamps the §2 classifier's corrected types onto the mined terms, so the BANKED type
|
||||
// is the re-derived one, not the draft heuristic. It never changes which terms are in the delta — emission
|
||||
// eligibility was already decided upstream by the miner — only the type recorded on the rows already there.
|
||||
func attachClassifiedType(mined []miner.Term, classified map[string]string) []miner.Term {
|
||||
if len(classified) == 0 {
|
||||
return mined
|
||||
}
|
||||
out := make([]miner.Term, len(mined))
|
||||
copy(out, mined)
|
||||
for i := range out {
|
||||
if t := classified[text.NormalizeSourceKey(out[i].Src)]; t != "" {
|
||||
out[i].Type = t
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
==== run 1 (fresh) ====
|
||||
snapshot_draft: e66ae9aaac35d5f551473902e40fc93b5c55c123b39d5fccd426b30003def799
|
||||
snapshot_draft_payload: {"brief_hash":"c2021b1b5e6c20f6084e425b9a70bdc5f682801be347a57eb1c591deb8cab0df","chunker_version":"chunker-v6-generic-heading+srcabbrev+u15.0.0","estimator_version":"estimator-v0+u15.0.0","max_tokens_policy":"maxtok-v1-double-per-attempt","classifier_version":"classify-v2-refusal+srcscript-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-v3-draft-gender+editor-src2dst+dc3-gender","embedded_version":"embed-v1-b0c8d2e685a2","memory_version":"bd451833d4ebb8a0ca07caf8deda76f027e1f42b78654987a22d215acf955eb7","postcheck_gate":false,"coverage":{"enabled":false},"style_check_version":"cheapgate-v6-target-tokenizer-combmark+u15.0.0","sanitizer":{"enabled":true,"version":"sanitizer-v7-hangul-leak"},"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: eba3eaa34c955bb190782fe28f490d50038757576aebaef6303b9b7b364fe6c5
|
||||
snapshot_edit_payload: {"brief_hash":"c2021b1b5e6c20f6084e425b9a70bdc5f682801be347a57eb1c591deb8cab0df","chunker_version":"chunker-v6-generic-heading+srcabbrev+u15.0.0","estimator_version":"estimator-v0+u15.0.0","max_tokens_policy":"maxtok-v1-double-per-attempt","classifier_version":"classify-v2-refusal+srcscript-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-v3-draft-gender+editor-src2dst+dc3-gender","embedded_version":"embed-v1-b0c8d2e685a2","memory_version":"0a48d6ed9409de09f514af91aebdd622637621c9dc9464e8898c238791033d91","postcheck_gate":false,"coverage":{"enabled":false},"style_check_version":"cheapgate-v6-target-tokenizer-combmark+u15.0.0","sanitizer":{"enabled":true,"version":"sanitizer-v7-hangul-leak"},"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: dc7572ca82f1898969220aecbb5727c36b9a42486ee70237ae8f95b8fe9ebce4
|
||||
snapshot_draft_payload: {"brief_hash":"c2021b1b5e6c20f6084e425b9a70bdc5f682801be347a57eb1c591deb8cab0df","chunker_version":"chunker-v6-generic-heading+srcabbrev+u15.0.0","estimator_version":"estimator-v0+u15.0.0","max_tokens_policy":"maxtok-v1-double-per-attempt","classifier_version":"classify-v2-refusal+srcscript-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-v3-draft-gender+editor-src2dst+dc3-gender","embedded_version":"embed-v1-37bf9699835f","memory_version":"bd451833d4ebb8a0ca07caf8deda76f027e1f42b78654987a22d215acf955eb7","postcheck_gate":false,"coverage":{"enabled":false},"style_check_version":"cheapgate-v6-target-tokenizer-combmark+u15.0.0","sanitizer":{"enabled":true,"version":"sanitizer-v7-hangul-leak"},"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: 6799e44bbf02e21419447613ec3915f9f47c61f2746b94c1fc898ec9f5aa70bf
|
||||
snapshot_edit_payload: {"brief_hash":"c2021b1b5e6c20f6084e425b9a70bdc5f682801be347a57eb1c591deb8cab0df","chunker_version":"chunker-v6-generic-heading+srcabbrev+u15.0.0","estimator_version":"estimator-v0+u15.0.0","max_tokens_policy":"maxtok-v1-double-per-attempt","classifier_version":"classify-v2-refusal+srcscript-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-v3-draft-gender+editor-src2dst+dc3-gender","embedded_version":"embed-v1-37bf9699835f","memory_version":"0a48d6ed9409de09f514af91aebdd622637621c9dc9464e8898c238791033d91","postcheck_gate":false,"coverage":{"enabled":false},"style_check_version":"cheapgate-v6-target-tokenizer-combmark+u15.0.0","sanitizer":{"enabled":true,"version":"sanitizer-v7-hangul-leak"},"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: 0a48d6ed9409de09f514af91aebdd622637621c9dc9464e8898c238791033d91
|
||||
base_memory_version: bd451833d4ebb8a0ca07caf8deda76f027e1f42b78654987a22d215acf955eb7
|
||||
|
|
@ -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 leak in the ru output: 特产"
|
||||
stage_text="" recovered="Судзуки нашёл древний камень на каменном алтаре."
|
||||
-- chunk_status --
|
||||
ch1/0 draft snap_match=true content_hash=3e156226b9a74aa70c8a259c62e3d2fe68486a96d2178d31084c1e38a3256241 disp=ok flag="" attempts=1 final_hash=c359e411715aace7c3473339e1045cc2267d14e72166d074eca6d6829294756c cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch1/0 edit snap_match=true content_hash=5e79dd5cbd310ac01d71ca258002beca18db715dfb8b1a08907508e506595355 disp=ok flag="" attempts=1 final_hash=4a75bc83c65ceb170c71a0f52424ebf247db0f8915890488010fa60def476cb3 cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch1/1 draft snap_match=true content_hash=82fb7f985de6c07182e8c09a3ceaad94f2e63745b87024087e437fba762b8227 disp=ok flag="" attempts=1 final_hash=e12b4c9178b42841386fb9f41ade6324888b890ae0795829df0cbaa93d8f286f cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch2/0 draft snap_match=true content_hash=cfab1e09264a3a30aba95e7da1d6a269468c6aa0c1b334d9241e78bc266a3571 disp=ok flag="" attempts=1 final_hash=c50b649e6b432bca99f7f9733521587dcf074ead85c0776bb3ff057cfbb306b5 cost=0.00728 escalated=true esc_model="fake-fallback" detail=""
|
||||
ch2/0 edit snap_match=true content_hash=55ffd839a4a26c03cf57e08000ab2c84b267e1429ab4a8e212d3a01fda757632 disp=ok flag="" attempts=1 final_hash=5aec367f3096017ab4d46ee5ea776c0f5a6907f8d9c64e054bc54bc8d0485eb4 cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch1/0 draft snap_match=true content_hash=3e156226b9a74aa70c8a259c62e3d2fe68486a96d2178d31084c1e38a3256241 disp=ok flag="" attempts=1 final_hash=70bca3400d2d8d848af27845cb2b86f362cdcf7a3a73e2611c46ec05feb7c4c2 cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch1/0 edit snap_match=true content_hash=5e79dd5cbd310ac01d71ca258002beca18db715dfb8b1a08907508e506595355 disp=ok flag="" attempts=1 final_hash=799b6e6419910a2e52dd4652f2bde9bee59fbdb92a849f291ed33a5e30410a15 cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch1/1 draft snap_match=true content_hash=82fb7f985de6c07182e8c09a3ceaad94f2e63745b87024087e437fba762b8227 disp=ok flag="" attempts=1 final_hash=ad47812002136f356e16d4fe4d71b73fdf1f2fbbe4f6a2cecd696abb07b7502d cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch2/0 draft snap_match=true content_hash=cfab1e09264a3a30aba95e7da1d6a269468c6aa0c1b334d9241e78bc266a3571 disp=ok flag="" attempts=1 final_hash=35d5f692c908d3c15b40757a167c0deb3067e90492a4490500b0a0ccf359c9e2 cost=0.00728 escalated=true esc_model="fake-fallback" detail=""
|
||||
ch2/0 edit snap_match=true content_hash=55ffd839a4a26c03cf57e08000ab2c84b267e1429ab4a8e212d3a01fda757632 disp=ok flag="" attempts=1 final_hash=e3f8acb903b0dd5798636597412acadfcf03a968ff049a8604b1142dea5cded1 cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch3/0 draft snap_match=true content_hash=21df44eb104900664bdaaeb2f716f51dcddbc81a71d845f6ba0f91c5bc4b2cfb disp=flagged flag="hard_refusal" attempts=1 final_hash= cost=0.00728 escalated=true esc_model="" 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=1880b22b82a38a8a475a4d4d1457aa14c11d5669990ffaa1f0c311bf75ebf9c7 cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch4/0 edit snap_match=true content_hash=2854dfe1648b0cf0cbb683cb7ce94c5eedc9223b2ce20c99dc008c7add6317ef disp=ok flag="" attempts=1 final_hash=a30dd9aebaed1fe56614e4d37e9e32959a6ace1a845a513ead6ca5a6797005c4 cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch5/0 draft snap_match=true content_hash=2ed0a97f39cc5c57c62c3324f4646357372f26e3c5bae1773f24edf9449fc013 disp=ok flag="" attempts=1 final_hash=13b70142e64657da9c78d9d8da8f44de28b627e70823c34e800ebdba1dc5cc39 cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch5/0 edit snap_match=true content_hash=b145d78e4d30a39cc4088a1ddb8d934234c65341c62c79545c2eff1e7ae0c3d3 disp=ok flag="" attempts=1 final_hash=cef18113a5c9f1386418ad27029e541c3ef3bf187032cd6f6debc936bbe40cc0 cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch6/0 draft snap_match=true content_hash=5f396d299a7ec5b10f3c8591e54ce7d84c4d31b815e561b0dd5f56635aef5a71 disp=ok flag="" attempts=1 final_hash=fb16ddecec38450b712d9ec1e3435b150a588e382e7808b148653107566080b6 cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch4/0 draft snap_match=true content_hash=7aabdcb89dea365c95363b2662144ca1a10b307d377f47a8e7cc19b5b872d287 disp=ok flag="" attempts=1 final_hash=c74a4b18be42dae5946fc9d33f1172e6be71a37bad7dc08076e5fba59316e858 cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch4/0 edit snap_match=true content_hash=2854dfe1648b0cf0cbb683cb7ce94c5eedc9223b2ce20c99dc008c7add6317ef disp=ok flag="" attempts=1 final_hash=5f8f5737a4e02766f8500915341c8b8a8827f8a05b03d77a746a4a8bc48806eb cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch5/0 draft snap_match=true content_hash=2ed0a97f39cc5c57c62c3324f4646357372f26e3c5bae1773f24edf9449fc013 disp=ok flag="" attempts=1 final_hash=407f023c09e54b0399f79cf59e893817065b28129a807c57b25cc796202d75e1 cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch5/0 edit snap_match=true content_hash=b145d78e4d30a39cc4088a1ddb8d934234c65341c62c79545c2eff1e7ae0c3d3 disp=ok flag="" attempts=1 final_hash=2b722376f3691a64c38d9a8026f567dfce0dd0a5ca3d2355bd2a95e4807ee67c cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch6/0 draft snap_match=true content_hash=5f396d299a7ec5b10f3c8591e54ce7d84c4d31b815e561b0dd5f56635aef5a71 disp=ok flag="" attempts=1 final_hash=afd2169fce4c9aaecdf7f3de1f1f87bc08d5b672d19bc1dc772991f8d3321d60 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="leading service preamble: Вот перевод фрагмента:"
|
||||
ch7/0 draft snap_match=true content_hash=15c3ebbd56bb19880202d35b0206357655da245343376492f7ceb61808c8a5be disp=ok flag="" attempts=1 final_hash=09fdfda22e00061376cef8a50eb005c7b154b10f9808ed12ab2b1d5a54012865 cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch7/0 edit snap_match=true content_hash=0b2674ed9829b2e6526ff41a60dd280b6670c539476526bbe835d9bf7a6be4c4 disp=flagged flag="sanitizer_stripped" attempts=1 final_hash=tm-sanitized-v1:93c49c0115fab1b8ac52b94bf513b2c119a7dc44f7fa50b59c8a8a2843a0f373 cost=0.00182 escalated=false esc_model="" detail="markdown header in the output: ### Глава 7"
|
||||
ch8/0 draft snap_match=true content_hash=4a420977cc34ce738b4978e6a1baf45d78d773a135070c3b7f8c723adfa57d7b disp=ok flag="" attempts=1 final_hash=c93524c990432bbc36a9866e2a22e9e1ea5b6e6a6bf08b282bde0d79a6bb3f36 cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch8/0 edit snap_match=true content_hash=c851100c8d6785f77d68f8127472790ae9d0408b7fec44785d3ff9d46842c6f8 disp=flagged flag="sanitizer_stripped" attempts=1 final_hash=tm-sanitized-v1:67de3e29186115d82cbc899002b3e70439f7d85f3794906d3b32bce9ab8b4aea cost=0.00182 escalated=false esc_model="" detail="CJK leak in the ru output: 特产"
|
||||
ch7/0 draft snap_match=true content_hash=15c3ebbd56bb19880202d35b0206357655da245343376492f7ceb61808c8a5be disp=ok flag="" attempts=1 final_hash=486facff54a77c86f44cd322200caf4cf982975eeda552fa6dfdee4aa8df0904 cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch7/0 edit snap_match=true content_hash=0b2674ed9829b2e6526ff41a60dd280b6670c539476526bbe835d9bf7a6be4c4 disp=flagged flag="sanitizer_stripped" attempts=1 final_hash=tm-sanitized-v1:def877b81bcdc52e2525fdcaae04b8c64f632c7d6747b31df7f4c7f1ad176e73 cost=0.00182 escalated=false esc_model="" detail="markdown header in the output: ### Глава 7"
|
||||
ch8/0 draft snap_match=true content_hash=4a420977cc34ce738b4978e6a1baf45d78d773a135070c3b7f8c723adfa57d7b disp=ok flag="" attempts=1 final_hash=070d13df73e028346b69abe69253a999f109d92b01f39f3dfada0772be50e185 cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch8/0 edit snap_match=true content_hash=c851100c8d6785f77d68f8127472790ae9d0408b7fec44785d3ff9d46842c6f8 disp=flagged flag="sanitizer_stripped" attempts=1 final_hash=tm-sanitized-v1:5095da45458381184b8798287c934e133c39668fed86c50cce921a1cb7df7893 cost=0.00182 escalated=false esc_model="" detail="CJK leak in the ru output: 特产"
|
||||
-- request_log (insertion order) --
|
||||
ch1/0 draft role=translator req=fake-model actual=fake-model hash=c359e411715aace7c3473339e1045cc2267d14e72166d074eca6d6829294756c 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=e12b4c9178b42841386fb9f41ade6324888b890ae0795829df0cbaa93d8f286f 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=ff97014d98b89f2b82696792cd28e903d0017ae0a95b0efde2b523f8d9579bc5 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=c50b649e6b432bca99f7f9733521587dcf074ead85c0776bb3ff057cfbb306b5 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=195edb6c4c2762b4645071493007c4db83969b3b0af41095a69e40bbf030830f 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=365cad742ce4ecaa1af247b74c4bf1c04da6ab15565aae9285442f8ee7011882 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=1880b22b82a38a8a475a4d4d1457aa14c11d5669990ffaa1f0c311bf75ebf9c7 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=13b70142e64657da9c78d9d8da8f44de28b627e70823c34e800ebdba1dc5cc39 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=fb16ddecec38450b712d9ec1e3435b150a588e382e7808b148653107566080b6 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=09fdfda22e00061376cef8a50eb005c7b154b10f9808ed12ab2b1d5a54012865 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=c93524c990432bbc36a9866e2a22e9e1ea5b6e6a6bf08b282bde0d79a6bb3f36 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=4a75bc83c65ceb170c71a0f52424ebf247db0f8915890488010fa60def476cb3 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=5aec367f3096017ab4d46ee5ea776c0f5a6907f8d9c64e054bc54bc8d0485eb4 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=a30dd9aebaed1fe56614e4d37e9e32959a6ace1a845a513ead6ca5a6797005c4 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=cef18113a5c9f1386418ad27029e541c3ef3bf187032cd6f6debc936bbe40cc0 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=0b684110fc1d2557ce1a8ad696146e52ea2b3dce1f409b408acc40e4ee6ea7cb 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=d2de4948d9933fc2fbaa05f4f66f030fd6f0b245bd338ed6e81a598d55705fee 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=d29763de86c743a4c8c4d3963705a82ada3db29220c4953c0c990446f48e86d1 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=70bca3400d2d8d848af27845cb2b86f362cdcf7a3a73e2611c46ec05feb7c4c2 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=ad47812002136f356e16d4fe4d71b73fdf1f2fbbe4f6a2cecd696abb07b7502d 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=56b4390260a43c1ffd2ba17afb8c3fab16113992856bb960e62e11c4937e0028 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=35d5f692c908d3c15b40757a167c0deb3067e90492a4490500b0a0ccf359c9e2 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=9e6325591eca8bc28e34be1da64106704d9589193c991f6545b22e8ddbae3b54 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=544c921f47e59718019cfff42782f590deba18352123ae0687ceade0130fe2bd 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=c74a4b18be42dae5946fc9d33f1172e6be71a37bad7dc08076e5fba59316e858 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=407f023c09e54b0399f79cf59e893817065b28129a807c57b25cc796202d75e1 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=afd2169fce4c9aaecdf7f3de1f1f87bc08d5b672d19bc1dc772991f8d3321d60 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=486facff54a77c86f44cd322200caf4cf982975eeda552fa6dfdee4aa8df0904 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=070d13df73e028346b69abe69253a999f109d92b01f39f3dfada0772be50e185 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=799b6e6419910a2e52dd4652f2bde9bee59fbdb92a849f291ed33a5e30410a15 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=e3f8acb903b0dd5798636597412acadfcf03a968ff049a8604b1142dea5cded1 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=5f8f5737a4e02766f8500915341c8b8a8827f8a05b03d77a746a4a8bc48806eb 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=2b722376f3691a64c38d9a8026f567dfce0dd0a5ca3d2355bd2a95e4807ee67c 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=806e0871ca091d6ad598412d1ed78c711c0bc19412b1c20599d25ea1a7d3dc0f 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=c445cf04d138dd1ecfae1b805336f2c5cde333b575b64f75c10f1017e39ac498 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=1a3bc5b91428e2cc31a965748ea10046bf6d9f420f8c8d3e79f9fd3da5b03f4f 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,10 +126,10 @@ 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":"Черновик перевода для редактуры: ЧЕРНОВИК ПЕРЕВОДА 9e65df37b751. МАРКДАУН-СЦЕНА Судзуки открыл седьмую дверь."}],"model":"fake-model","stream":false,"temperature":0.4}
|
||||
[17] {"max_tokens":4000,"messages":[{"role":"system","content":"Ты — монолингвальный редактор русского текста. Правь стиль, не меняя смысла. Книга: Золотая книга."},{"role":"system","content":"КАНОНИЧЕСКИЕ ПЕРЕВОДЫ имён и терминов (в черновике термин исходника слева ДОЛЖЕН быть передан именно указанной формой справа — приводи к ней любые расхождения, склоняя по контексту; не вводи иных вариантов и не меняй ничего другого):\n- 鈴木 → «Судзуки» (муж. — мужские родовые формы)"},{"role":"user","content":"Черновик перевода для редактуры: ЧЕРНОВИК ПЕРЕВОДА 2c858e3366d2. ИЕРОГЛИФ-СЦЕНА Судзуки нашёл камень."}],"model":"fake-model","stream":false,"temperature":0.4}
|
||||
==== run 2 (resume) ====
|
||||
snapshot_draft: e66ae9aaac35d5f551473902e40fc93b5c55c123b39d5fccd426b30003def799
|
||||
snapshot_draft_payload: {"brief_hash":"c2021b1b5e6c20f6084e425b9a70bdc5f682801be347a57eb1c591deb8cab0df","chunker_version":"chunker-v6-generic-heading+srcabbrev+u15.0.0","estimator_version":"estimator-v0+u15.0.0","max_tokens_policy":"maxtok-v1-double-per-attempt","classifier_version":"classify-v2-refusal+srcscript-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-v3-draft-gender+editor-src2dst+dc3-gender","embedded_version":"embed-v1-b0c8d2e685a2","memory_version":"bd451833d4ebb8a0ca07caf8deda76f027e1f42b78654987a22d215acf955eb7","postcheck_gate":false,"coverage":{"enabled":false},"style_check_version":"cheapgate-v6-target-tokenizer-combmark+u15.0.0","sanitizer":{"enabled":true,"version":"sanitizer-v7-hangul-leak"},"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: eba3eaa34c955bb190782fe28f490d50038757576aebaef6303b9b7b364fe6c5
|
||||
snapshot_edit_payload: {"brief_hash":"c2021b1b5e6c20f6084e425b9a70bdc5f682801be347a57eb1c591deb8cab0df","chunker_version":"chunker-v6-generic-heading+srcabbrev+u15.0.0","estimator_version":"estimator-v0+u15.0.0","max_tokens_policy":"maxtok-v1-double-per-attempt","classifier_version":"classify-v2-refusal+srcscript-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-v3-draft-gender+editor-src2dst+dc3-gender","embedded_version":"embed-v1-b0c8d2e685a2","memory_version":"0a48d6ed9409de09f514af91aebdd622637621c9dc9464e8898c238791033d91","postcheck_gate":false,"coverage":{"enabled":false},"style_check_version":"cheapgate-v6-target-tokenizer-combmark+u15.0.0","sanitizer":{"enabled":true,"version":"sanitizer-v7-hangul-leak"},"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: dc7572ca82f1898969220aecbb5727c36b9a42486ee70237ae8f95b8fe9ebce4
|
||||
snapshot_draft_payload: {"brief_hash":"c2021b1b5e6c20f6084e425b9a70bdc5f682801be347a57eb1c591deb8cab0df","chunker_version":"chunker-v6-generic-heading+srcabbrev+u15.0.0","estimator_version":"estimator-v0+u15.0.0","max_tokens_policy":"maxtok-v1-double-per-attempt","classifier_version":"classify-v2-refusal+srcscript-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-v3-draft-gender+editor-src2dst+dc3-gender","embedded_version":"embed-v1-37bf9699835f","memory_version":"bd451833d4ebb8a0ca07caf8deda76f027e1f42b78654987a22d215acf955eb7","postcheck_gate":false,"coverage":{"enabled":false},"style_check_version":"cheapgate-v6-target-tokenizer-combmark+u15.0.0","sanitizer":{"enabled":true,"version":"sanitizer-v7-hangul-leak"},"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: 6799e44bbf02e21419447613ec3915f9f47c61f2746b94c1fc898ec9f5aa70bf
|
||||
snapshot_edit_payload: {"brief_hash":"c2021b1b5e6c20f6084e425b9a70bdc5f682801be347a57eb1c591deb8cab0df","chunker_version":"chunker-v6-generic-heading+srcabbrev+u15.0.0","estimator_version":"estimator-v0+u15.0.0","max_tokens_policy":"maxtok-v1-double-per-attempt","classifier_version":"classify-v2-refusal+srcscript-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-v3-draft-gender+editor-src2dst+dc3-gender","embedded_version":"embed-v1-37bf9699835f","memory_version":"0a48d6ed9409de09f514af91aebdd622637621c9dc9464e8898c238791033d91","postcheck_gate":false,"coverage":{"enabled":false},"style_check_version":"cheapgate-v6-target-tokenizer-combmark+u15.0.0","sanitizer":{"enabled":true,"version":"sanitizer-v7-hangul-leak"},"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: 0a48d6ed9409de09f514af91aebdd622637621c9dc9464e8898c238791033d91
|
||||
base_memory_version: bd451833d4ebb8a0ca07caf8deda76f027e1f42b78654987a22d215acf955eb7
|
||||
|
|
@ -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 leak in the ru output: 特产"
|
||||
stage_text="" recovered="Судзуки нашёл древний камень на каменном алтаре."
|
||||
-- chunk_status --
|
||||
ch1/0 draft snap_match=true content_hash=3e156226b9a74aa70c8a259c62e3d2fe68486a96d2178d31084c1e38a3256241 disp=ok flag="" attempts=1 final_hash=c359e411715aace7c3473339e1045cc2267d14e72166d074eca6d6829294756c cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch1/0 edit snap_match=true content_hash=5e79dd5cbd310ac01d71ca258002beca18db715dfb8b1a08907508e506595355 disp=ok flag="" attempts=1 final_hash=4a75bc83c65ceb170c71a0f52424ebf247db0f8915890488010fa60def476cb3 cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch1/1 draft snap_match=true content_hash=82fb7f985de6c07182e8c09a3ceaad94f2e63745b87024087e437fba762b8227 disp=ok flag="" attempts=1 final_hash=e12b4c9178b42841386fb9f41ade6324888b890ae0795829df0cbaa93d8f286f cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch2/0 draft snap_match=true content_hash=cfab1e09264a3a30aba95e7da1d6a269468c6aa0c1b334d9241e78bc266a3571 disp=ok flag="" attempts=1 final_hash=c50b649e6b432bca99f7f9733521587dcf074ead85c0776bb3ff057cfbb306b5 cost=0.00728 escalated=true esc_model="fake-fallback" detail=""
|
||||
ch2/0 edit snap_match=true content_hash=55ffd839a4a26c03cf57e08000ab2c84b267e1429ab4a8e212d3a01fda757632 disp=ok flag="" attempts=1 final_hash=5aec367f3096017ab4d46ee5ea776c0f5a6907f8d9c64e054bc54bc8d0485eb4 cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch1/0 draft snap_match=true content_hash=3e156226b9a74aa70c8a259c62e3d2fe68486a96d2178d31084c1e38a3256241 disp=ok flag="" attempts=1 final_hash=70bca3400d2d8d848af27845cb2b86f362cdcf7a3a73e2611c46ec05feb7c4c2 cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch1/0 edit snap_match=true content_hash=5e79dd5cbd310ac01d71ca258002beca18db715dfb8b1a08907508e506595355 disp=ok flag="" attempts=1 final_hash=799b6e6419910a2e52dd4652f2bde9bee59fbdb92a849f291ed33a5e30410a15 cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch1/1 draft snap_match=true content_hash=82fb7f985de6c07182e8c09a3ceaad94f2e63745b87024087e437fba762b8227 disp=ok flag="" attempts=1 final_hash=ad47812002136f356e16d4fe4d71b73fdf1f2fbbe4f6a2cecd696abb07b7502d cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch2/0 draft snap_match=true content_hash=cfab1e09264a3a30aba95e7da1d6a269468c6aa0c1b334d9241e78bc266a3571 disp=ok flag="" attempts=1 final_hash=35d5f692c908d3c15b40757a167c0deb3067e90492a4490500b0a0ccf359c9e2 cost=0.00728 escalated=true esc_model="fake-fallback" detail=""
|
||||
ch2/0 edit snap_match=true content_hash=55ffd839a4a26c03cf57e08000ab2c84b267e1429ab4a8e212d3a01fda757632 disp=ok flag="" attempts=1 final_hash=e3f8acb903b0dd5798636597412acadfcf03a968ff049a8604b1142dea5cded1 cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch3/0 draft snap_match=true content_hash=21df44eb104900664bdaaeb2f716f51dcddbc81a71d845f6ba0f91c5bc4b2cfb disp=flagged flag="hard_refusal" attempts=1 final_hash= cost=0.00728 escalated=true esc_model="" 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=1880b22b82a38a8a475a4d4d1457aa14c11d5669990ffaa1f0c311bf75ebf9c7 cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch4/0 edit snap_match=true content_hash=2854dfe1648b0cf0cbb683cb7ce94c5eedc9223b2ce20c99dc008c7add6317ef disp=ok flag="" attempts=1 final_hash=a30dd9aebaed1fe56614e4d37e9e32959a6ace1a845a513ead6ca5a6797005c4 cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch5/0 draft snap_match=true content_hash=2ed0a97f39cc5c57c62c3324f4646357372f26e3c5bae1773f24edf9449fc013 disp=ok flag="" attempts=1 final_hash=13b70142e64657da9c78d9d8da8f44de28b627e70823c34e800ebdba1dc5cc39 cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch5/0 edit snap_match=true content_hash=b145d78e4d30a39cc4088a1ddb8d934234c65341c62c79545c2eff1e7ae0c3d3 disp=ok flag="" attempts=1 final_hash=cef18113a5c9f1386418ad27029e541c3ef3bf187032cd6f6debc936bbe40cc0 cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch6/0 draft snap_match=true content_hash=5f396d299a7ec5b10f3c8591e54ce7d84c4d31b815e561b0dd5f56635aef5a71 disp=ok flag="" attempts=1 final_hash=fb16ddecec38450b712d9ec1e3435b150a588e382e7808b148653107566080b6 cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch4/0 draft snap_match=true content_hash=7aabdcb89dea365c95363b2662144ca1a10b307d377f47a8e7cc19b5b872d287 disp=ok flag="" attempts=1 final_hash=c74a4b18be42dae5946fc9d33f1172e6be71a37bad7dc08076e5fba59316e858 cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch4/0 edit snap_match=true content_hash=2854dfe1648b0cf0cbb683cb7ce94c5eedc9223b2ce20c99dc008c7add6317ef disp=ok flag="" attempts=1 final_hash=5f8f5737a4e02766f8500915341c8b8a8827f8a05b03d77a746a4a8bc48806eb cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch5/0 draft snap_match=true content_hash=2ed0a97f39cc5c57c62c3324f4646357372f26e3c5bae1773f24edf9449fc013 disp=ok flag="" attempts=1 final_hash=407f023c09e54b0399f79cf59e893817065b28129a807c57b25cc796202d75e1 cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch5/0 edit snap_match=true content_hash=b145d78e4d30a39cc4088a1ddb8d934234c65341c62c79545c2eff1e7ae0c3d3 disp=ok flag="" attempts=1 final_hash=2b722376f3691a64c38d9a8026f567dfce0dd0a5ca3d2355bd2a95e4807ee67c cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch6/0 draft snap_match=true content_hash=5f396d299a7ec5b10f3c8591e54ce7d84c4d31b815e561b0dd5f56635aef5a71 disp=ok flag="" attempts=1 final_hash=afd2169fce4c9aaecdf7f3de1f1f87bc08d5b672d19bc1dc772991f8d3321d60 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="leading service preamble: Вот перевод фрагмента:"
|
||||
ch7/0 draft snap_match=true content_hash=15c3ebbd56bb19880202d35b0206357655da245343376492f7ceb61808c8a5be disp=ok flag="" attempts=1 final_hash=09fdfda22e00061376cef8a50eb005c7b154b10f9808ed12ab2b1d5a54012865 cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch7/0 edit snap_match=true content_hash=0b2674ed9829b2e6526ff41a60dd280b6670c539476526bbe835d9bf7a6be4c4 disp=flagged flag="sanitizer_stripped" attempts=1 final_hash=tm-sanitized-v1:93c49c0115fab1b8ac52b94bf513b2c119a7dc44f7fa50b59c8a8a2843a0f373 cost=0.00182 escalated=false esc_model="" detail="markdown header in the output: ### Глава 7"
|
||||
ch8/0 draft snap_match=true content_hash=4a420977cc34ce738b4978e6a1baf45d78d773a135070c3b7f8c723adfa57d7b disp=ok flag="" attempts=1 final_hash=c93524c990432bbc36a9866e2a22e9e1ea5b6e6a6bf08b282bde0d79a6bb3f36 cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch8/0 edit snap_match=true content_hash=c851100c8d6785f77d68f8127472790ae9d0408b7fec44785d3ff9d46842c6f8 disp=flagged flag="sanitizer_stripped" attempts=1 final_hash=tm-sanitized-v1:67de3e29186115d82cbc899002b3e70439f7d85f3794906d3b32bce9ab8b4aea cost=0.00182 escalated=false esc_model="" detail="CJK leak in the ru output: 特产"
|
||||
ch7/0 draft snap_match=true content_hash=15c3ebbd56bb19880202d35b0206357655da245343376492f7ceb61808c8a5be disp=ok flag="" attempts=1 final_hash=486facff54a77c86f44cd322200caf4cf982975eeda552fa6dfdee4aa8df0904 cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch7/0 edit snap_match=true content_hash=0b2674ed9829b2e6526ff41a60dd280b6670c539476526bbe835d9bf7a6be4c4 disp=flagged flag="sanitizer_stripped" attempts=1 final_hash=tm-sanitized-v1:def877b81bcdc52e2525fdcaae04b8c64f632c7d6747b31df7f4c7f1ad176e73 cost=0.00182 escalated=false esc_model="" detail="markdown header in the output: ### Глава 7"
|
||||
ch8/0 draft snap_match=true content_hash=4a420977cc34ce738b4978e6a1baf45d78d773a135070c3b7f8c723adfa57d7b disp=ok flag="" attempts=1 final_hash=070d13df73e028346b69abe69253a999f109d92b01f39f3dfada0772be50e185 cost=0.00182 escalated=false esc_model="" detail=""
|
||||
ch8/0 edit snap_match=true content_hash=c851100c8d6785f77d68f8127472790ae9d0408b7fec44785d3ff9d46842c6f8 disp=flagged flag="sanitizer_stripped" attempts=1 final_hash=tm-sanitized-v1:5095da45458381184b8798287c934e133c39668fed86c50cce921a1cb7df7893 cost=0.00182 escalated=false esc_model="" detail="CJK leak in the ru output: 特产"
|
||||
-- request_log (insertion order) --
|
||||
ch1/0 draft role=translator req=fake-model actual=fake-model hash=c359e411715aace7c3473339e1045cc2267d14e72166d074eca6d6829294756c 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=e12b4c9178b42841386fb9f41ade6324888b890ae0795829df0cbaa93d8f286f 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=ff97014d98b89f2b82696792cd28e903d0017ae0a95b0efde2b523f8d9579bc5 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=c50b649e6b432bca99f7f9733521587dcf074ead85c0776bb3ff057cfbb306b5 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=195edb6c4c2762b4645071493007c4db83969b3b0af41095a69e40bbf030830f 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=365cad742ce4ecaa1af247b74c4bf1c04da6ab15565aae9285442f8ee7011882 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=1880b22b82a38a8a475a4d4d1457aa14c11d5669990ffaa1f0c311bf75ebf9c7 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=13b70142e64657da9c78d9d8da8f44de28b627e70823c34e800ebdba1dc5cc39 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=fb16ddecec38450b712d9ec1e3435b150a588e382e7808b148653107566080b6 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=09fdfda22e00061376cef8a50eb005c7b154b10f9808ed12ab2b1d5a54012865 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=c93524c990432bbc36a9866e2a22e9e1ea5b6e6a6bf08b282bde0d79a6bb3f36 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=4a75bc83c65ceb170c71a0f52424ebf247db0f8915890488010fa60def476cb3 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=5aec367f3096017ab4d46ee5ea776c0f5a6907f8d9c64e054bc54bc8d0485eb4 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=a30dd9aebaed1fe56614e4d37e9e32959a6ace1a845a513ead6ca5a6797005c4 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=cef18113a5c9f1386418ad27029e541c3ef3bf187032cd6f6debc936bbe40cc0 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=0b684110fc1d2557ce1a8ad696146e52ea2b3dce1f409b408acc40e4ee6ea7cb 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=d2de4948d9933fc2fbaa05f4f66f030fd6f0b245bd338ed6e81a598d55705fee 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=d29763de86c743a4c8c4d3963705a82ada3db29220c4953c0c990446f48e86d1 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=c359e411715aace7c3473339e1045cc2267d14e72166d074eca6d6829294756c 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=e12b4c9178b42841386fb9f41ade6324888b890ae0795829df0cbaa93d8f286f 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=c50b649e6b432bca99f7f9733521587dcf074ead85c0776bb3ff057cfbb306b5 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=70bca3400d2d8d848af27845cb2b86f362cdcf7a3a73e2611c46ec05feb7c4c2 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=ad47812002136f356e16d4fe4d71b73fdf1f2fbbe4f6a2cecd696abb07b7502d 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=56b4390260a43c1ffd2ba17afb8c3fab16113992856bb960e62e11c4937e0028 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=35d5f692c908d3c15b40757a167c0deb3067e90492a4490500b0a0ccf359c9e2 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=9e6325591eca8bc28e34be1da64106704d9589193c991f6545b22e8ddbae3b54 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=544c921f47e59718019cfff42782f590deba18352123ae0687ceade0130fe2bd 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=c74a4b18be42dae5946fc9d33f1172e6be71a37bad7dc08076e5fba59316e858 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=407f023c09e54b0399f79cf59e893817065b28129a807c57b25cc796202d75e1 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=afd2169fce4c9aaecdf7f3de1f1f87bc08d5b672d19bc1dc772991f8d3321d60 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=486facff54a77c86f44cd322200caf4cf982975eeda552fa6dfdee4aa8df0904 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=070d13df73e028346b69abe69253a999f109d92b01f39f3dfada0772be50e185 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=799b6e6419910a2e52dd4652f2bde9bee59fbdb92a849f291ed33a5e30410a15 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=e3f8acb903b0dd5798636597412acadfcf03a968ff049a8604b1142dea5cded1 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=5f8f5737a4e02766f8500915341c8b8a8827f8a05b03d77a746a4a8bc48806eb 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=2b722376f3691a64c38d9a8026f567dfce0dd0a5ca3d2355bd2a95e4807ee67c 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=806e0871ca091d6ad598412d1ed78c711c0bc19412b1c20599d25ea1a7d3dc0f 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=c445cf04d138dd1ecfae1b805336f2c5cde333b575b64f75c10f1017e39ac498 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=1a3bc5b91428e2cc31a965748ea10046bf6d9f420f8c8d3e79f9fd3da5b03f4f 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=70bca3400d2d8d848af27845cb2b86f362cdcf7a3a73e2611c46ec05feb7c4c2 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=ad47812002136f356e16d4fe4d71b73fdf1f2fbbe4f6a2cecd696abb07b7502d 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=35d5f692c908d3c15b40757a167c0deb3067e90492a4490500b0a0ccf359c9e2 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=1880b22b82a38a8a475a4d4d1457aa14c11d5669990ffaa1f0c311bf75ebf9c7 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=13b70142e64657da9c78d9d8da8f44de28b627e70823c34e800ebdba1dc5cc39 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=fb16ddecec38450b712d9ec1e3435b150a588e382e7808b148653107566080b6 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=09fdfda22e00061376cef8a50eb005c7b154b10f9808ed12ab2b1d5a54012865 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=c93524c990432bbc36a9866e2a22e9e1ea5b6e6a6bf08b282bde0d79a6bb3f36 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=4a75bc83c65ceb170c71a0f52424ebf247db0f8915890488010fa60def476cb3 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=5aec367f3096017ab4d46ee5ea776c0f5a6907f8d9c64e054bc54bc8d0485eb4 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=a30dd9aebaed1fe56614e4d37e9e32959a6ace1a845a513ead6ca5a6797005c4 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=cef18113a5c9f1386418ad27029e541c3ef3bf187032cd6f6debc936bbe40cc0 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=c74a4b18be42dae5946fc9d33f1172e6be71a37bad7dc08076e5fba59316e858 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=407f023c09e54b0399f79cf59e893817065b28129a807c57b25cc796202d75e1 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=afd2169fce4c9aaecdf7f3de1f1f87bc08d5b672d19bc1dc772991f8d3321d60 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=486facff54a77c86f44cd322200caf4cf982975eeda552fa6dfdee4aa8df0904 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=070d13df73e028346b69abe69253a999f109d92b01f39f3dfada0772be50e185 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=799b6e6419910a2e52dd4652f2bde9bee59fbdb92a849f291ed33a5e30410a15 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=e3f8acb903b0dd5798636597412acadfcf03a968ff049a8604b1142dea5cded1 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=5f8f5737a4e02766f8500915341c8b8a8827f8a05b03d77a746a4a8bc48806eb 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=2b722376f3691a64c38d9a8026f567dfce0dd0a5ca3d2355bd2a95e4807ee67c 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:93c49c0115fab1b8ac52b94bf513b2c119a7dc44f7fa50b59c8a8a2843a0f373 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:67de3e29186115d82cbc899002b3e70439f7d85f3794906d3b32bce9ab8b4aea 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:def877b81bcdc52e2525fdcaae04b8c64f632c7d6747b31df7f4c7f1ad176e73 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:5095da45458381184b8798287c934e133c39668fed86c50cce921a1cb7df7893 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=
|
||||
|
|
|
|||
76
backend/internal/terminology/classify.go
Normal file
76
backend/internal/terminology/classify.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
package terminology
|
||||
|
||||
import "strings"
|
||||
|
||||
// classify.go: the TYPE re-derivation channel (bank-quality §2, D39.68). The draft type heuristic is wrong
|
||||
// 12–22% (D39.65 row 36a), and type ∈ {name,place} routes a candidate to transliteration — so a realia
|
||||
// surface mistyped as a name (元石) is FORCED to «юаньши» instead of being translated. A focused classifier
|
||||
// pass fixes the type BEFORE the render (the live probe fixed 6/6 branch-harm units; an inline type returned
|
||||
// WITH the rendering arrives too late to de-bias it). This file is the pure half — the reply parser and the
|
||||
// honest $0 label screen; the class DEFINITIONS live in the pair's classifier prompt, no pair literal here.
|
||||
|
||||
// Types is the engine's closed type vocabulary. The names are engine identifiers (the miner emits them, the
|
||||
// glossary stores them, emissionEligible gates on them), so they are Go constants here, not pair data; the
|
||||
// pair's prompt explains each class in its own language with its own examples.
|
||||
var Types = map[string]bool{"name": true, "place": true, "title": true, "term": true}
|
||||
|
||||
// ParseTypes turns a classifier reply into key → corrected type, keeping only the terms we asked about and
|
||||
// only the closed vocabulary. Same tolerant two-field line format and the same accounting discipline as
|
||||
// ParseReply: an unusable or off-vocabulary line is COUNTED (st.Bad), never silently dropped, so a paid call
|
||||
// that bought no classification is loud rather than an empty map that reads as "nothing to correct".
|
||||
func ParseTypes(reply string, expected []string, normalize func(string) string) (map[string]string, ReplyStats) {
|
||||
want := make(map[string]bool, len(expected))
|
||||
for _, k := range expected {
|
||||
want[k] = true
|
||||
}
|
||||
out := map[string]string{}
|
||||
var st ReplyStats
|
||||
for _, ln := range strings.Split(reply, "\n") {
|
||||
if t := strings.TrimSpace(ln); t == "" || strings.HasPrefix(t, "#") {
|
||||
continue
|
||||
}
|
||||
f := splitFields(ln)
|
||||
if len(f) < 2 {
|
||||
st.Bad++
|
||||
continue
|
||||
}
|
||||
key := normalize(f[0])
|
||||
if !want[key] {
|
||||
st.Bad++
|
||||
continue
|
||||
}
|
||||
if _, dup := out[key]; dup {
|
||||
continue // first answer wins, as in ParseReply
|
||||
}
|
||||
typ := strings.ToLower(strings.TrimSpace(f[1]))
|
||||
if !Types[typ] {
|
||||
st.Bad++
|
||||
continue
|
||||
}
|
||||
out[key] = typ
|
||||
}
|
||||
return out, st
|
||||
}
|
||||
|
||||
// LabelRow is one row the $0 label screen inspects: the corrected type and the consolidated rendering.
|
||||
type LabelRow struct {
|
||||
Src string
|
||||
Type string
|
||||
Dst string
|
||||
}
|
||||
|
||||
// TypeLabelMismatches is the HONEST $0 screen (§2, warm-run hygiene). It flags a name/place row whose
|
||||
// rendering was clearly TRANSLATED (multi-word) — a label/rendering disagreement worth a human's eye. It is
|
||||
// explicitly NOT a safety net for the transliteration harm: a mistyped name rendered as a single lower-case
|
||||
// token (元石→юаньши) or a capitalised one (元海→Юаньхай) passes it clean, because the source class the harm
|
||||
// needs is exactly what a $0 pass cannot recover. The classifier pass is what prevents the harm; this only
|
||||
// surfaces leftover label noise for review. Deterministic, input order preserved.
|
||||
func TypeLabelMismatches(rows []LabelRow) []LabelRow {
|
||||
var out []LabelRow
|
||||
for _, r := range rows {
|
||||
if (r.Type == "name" || r.Type == "place") && strings.ContainsRune(strings.TrimSpace(r.Dst), ' ') {
|
||||
out = append(out, r)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
59
backend/internal/terminology/classify_test.go
Normal file
59
backend/internal/terminology/classify_test.go
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
package terminology
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseTypesKeepsOnlyAskedAndVocabulary(t *testing.T) {
|
||||
id := func(s string) string { return s }
|
||||
reply := strings.Join([]string{
|
||||
"元石\tterm",
|
||||
"方源 name", // spaces instead of a tab — tolerant split
|
||||
"青茅山 | place", // padded pipe
|
||||
"家老\tPERSON", // off-vocabulary → refused and counted
|
||||
"陌生\tterm", // never asked → refused and counted
|
||||
"元石\tname", // duplicate key → first answer wins
|
||||
"мусор", // one field → refused and counted
|
||||
}, "\n")
|
||||
got, st := ParseTypes(reply, []string{"元石", "方源", "青茅山", "家老"}, id)
|
||||
want := map[string]string{"元石": "term", "方源": "name", "青茅山": "place"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("parse = %#v, want %#v", got, want)
|
||||
}
|
||||
// off-vocabulary PERSON + unasked 陌生 + malformed мусор = 3 bad lines (the duplicate is silently ignored,
|
||||
// not counted, exactly like ParseReply).
|
||||
if st.Bad != 3 {
|
||||
t.Fatalf("unusable/off-vocabulary lines must be COUNTED, got %d", st.Bad)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTypesNormalizesKey(t *testing.T) {
|
||||
got, _ := ParseTypes("FANG\tname", []string{"fang"}, strings.ToLower)
|
||||
if got["fang"] != "name" {
|
||||
t.Fatalf("the reply key must be normalized by the caller's function, got %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTypeLabelMismatchesIsHonest pins the screen's DELIBERATE limit: it flags a name/place whose rendering
|
||||
// was clearly translated (multi-word), but it is NOT a safety net — a mistyped name rendered as one token,
|
||||
// lower-case (元石→юаньши) or capitalised (元海→Юаньхай), passes it clean. The classifier phase is what
|
||||
// prevents that harm; asserting the miss keeps a future reader from mistaking this for the guard.
|
||||
func TestTypeLabelMismatchesIsHonest(t *testing.T) {
|
||||
rows := []LabelRow{
|
||||
{Src: "花家", Type: "name", Dst: "Дом Хуа"}, // translated name → FLAG
|
||||
{Src: "青茅山", Type: "place", Dst: "гора Цинмао"}, // translated place → FLAG
|
||||
{Src: "元石", Type: "name", Dst: "юаньши"}, // the harm, single lower-case token → MISSED
|
||||
{Src: "元海", Type: "name", Dst: "Юаньхай"}, // the harm, capitalised token → MISSED
|
||||
{Src: "灵泉", Type: "term", Dst: "духовный источник"}, // term is not screened → not flagged
|
||||
}
|
||||
got := TypeLabelMismatches(rows)
|
||||
var srcs []string
|
||||
for _, r := range got {
|
||||
srcs = append(srcs, r.Src)
|
||||
}
|
||||
if !reflect.DeepEqual(srcs, []string{"花家", "青茅山"}) {
|
||||
t.Fatalf("only the translated name/place rows must flag, got %v", srcs)
|
||||
}
|
||||
}
|
||||
137
backend/internal/terminology/series.go
Normal file
137
backend/internal/terminology/series.go
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
package terminology
|
||||
|
||||
import "sort"
|
||||
|
||||
// series.go: the SERIES channel (bank-quality §1, D39.68). A grade/rank series — 甲等/乙等/丙等, 一转…九转 —
|
||||
// shares one generic HEAD and varies only on a modifier. Measured (D39.65 row 21): the drafts disagree on the
|
||||
// head across batch boundaries (甲等→«класс», 乙丙丁等→«ранг»), because Merge orders candidates by key and the
|
||||
// shared head sorts by its DIFFERING prefix. The fix (live probe) is co-batching — shown the set at once the
|
||||
// model picks ONE head; the first-occurrence test (§7.7) showed order/frequency tricks cannot do it, only
|
||||
// co-batching. This file detects the series; the batcher keeps each one whole in a single call.
|
||||
|
||||
// SeriesParams is the pair-data that governs the channel. It arrives as a value (the package stays pure and
|
||||
// pair-agnostic): the pipeline resolves it from the source language's declared morphology.
|
||||
type SeriesParams struct {
|
||||
// Enabled gates the whole channel. It is true only for DENSE scripts where one rune ≈ one morpheme, so a
|
||||
// single-rune difference is a real minimal pair; in an alphabetic source care/core differ in one letter by
|
||||
// coincidence, not by morphology, so the channel is off and Batch behaves exactly as before.
|
||||
Enabled bool
|
||||
// HeadFinal says the shared generic word is the TRAILING rune(s) (the CJK modifier-head norm). The differing
|
||||
// modifier rune is then non-final; with HeadFinal false the head is leading and the modifier is non-initial.
|
||||
HeadFinal bool
|
||||
// MinMembers is the smallest set that counts as a series (≤0 → 3). Below three a "series" is just a pair that
|
||||
// happens to share a character — too weak to override the key order for.
|
||||
MinMembers int
|
||||
}
|
||||
|
||||
// DetectSeries returns key → series id (>0) for every candidate that belongs to a head-aware series; a key in
|
||||
// no series is absent from the map. Empty when the channel is off.
|
||||
//
|
||||
// A series is ≥MinMembers surfaces of EQUAL rune length that are identical except in ONE rune position, and
|
||||
// that position is NOT the head (the last rune for HeadFinal, the first otherwise). So the members share a
|
||||
// generic head and vary only on the modifier — 甲等/乙等/丙等 (grades of 等). 元石/元海 differ IN the head
|
||||
// (石/海): different entities, the type step's job (§2), never one series. There is no transitive closure:
|
||||
// membership is by an exact shared skeleton, so 元气~酒气 (a shared head-rune 气 on two families) never chains
|
||||
// into a blob the way a naive "differ in one position" rule did (measured: an 11-surface blob mixing three
|
||||
// families and the protagonist's name).
|
||||
func DetectSeries(cands []Candidate, p SeriesParams) map[string]int {
|
||||
min := p.MinMembers
|
||||
if min <= 0 {
|
||||
min = 3
|
||||
}
|
||||
if !p.Enabled || len(cands) < min {
|
||||
return nil
|
||||
}
|
||||
// group[skeleton] = the distinct keys that reduce to it by blanking their one modifier position.
|
||||
type gkey struct {
|
||||
n, pos int
|
||||
skel string
|
||||
}
|
||||
groups := map[gkey][]string{}
|
||||
seenInGroup := map[gkey]map[string]bool{}
|
||||
var order []gkey // first-seen group order, for deterministic assignment
|
||||
for _, c := range cands {
|
||||
rs := []rune(c.Key)
|
||||
n := len(rs)
|
||||
if n < 2 { // a one-rune surface is all head, no modifier to vary
|
||||
continue
|
||||
}
|
||||
for pos := 0; pos < n; pos++ {
|
||||
if p.HeadFinal && pos == n-1 { // the last rune is (part of) the head
|
||||
continue
|
||||
}
|
||||
if !p.HeadFinal && pos == 0 {
|
||||
continue
|
||||
}
|
||||
g := gkey{n, pos, blankAt(rs, pos)}
|
||||
if seenInGroup[g] == nil {
|
||||
seenInGroup[g] = map[string]bool{}
|
||||
order = append(order, g)
|
||||
}
|
||||
if !seenInGroup[g][c.Key] {
|
||||
seenInGroup[g][c.Key] = true
|
||||
groups[g] = append(groups[g], c.Key)
|
||||
}
|
||||
}
|
||||
}
|
||||
// A key can satisfy several groups (varying at more than one modifier position). Assign biggest-first so
|
||||
// the strongest series claims its members; a group left below min after its members were claimed elsewhere
|
||||
// simply does not form. Deterministic: sort by descending size, then by first-seen order.
|
||||
sort.SliceStable(order, func(i, j int) bool { return len(groups[order[i]]) > len(groups[order[j]]) })
|
||||
out := map[string]int{}
|
||||
next := 1
|
||||
for _, g := range order {
|
||||
var fresh []string
|
||||
for _, k := range groups[g] {
|
||||
if out[k] == 0 {
|
||||
fresh = append(fresh, k)
|
||||
}
|
||||
}
|
||||
if len(fresh) >= min {
|
||||
for _, k := range fresh {
|
||||
out[k] = next
|
||||
}
|
||||
next++
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// blankAt returns the key with the rune at pos replaced by a byte no source key contains, so surfaces that
|
||||
// agree everywhere except pos share a skeleton and nothing else collides onto it.
|
||||
func blankAt(rs []rune, pos int) string {
|
||||
out := make([]rune, len(rs))
|
||||
copy(out, rs)
|
||||
out[pos] = 0
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// orderBySeries returns the candidates with every series' members made contiguous, anchored at the position
|
||||
// of the series' first member in the input order; non-series candidates keep their place. The input is
|
||||
// already key-sorted, so the result is deterministic and disturbs the key order minimally.
|
||||
func orderBySeries(cands []Candidate, seriesID map[string]int) []Candidate {
|
||||
if len(seriesID) == 0 {
|
||||
return cands
|
||||
}
|
||||
byID := map[int][]Candidate{}
|
||||
for _, c := range cands {
|
||||
if id := seriesID[c.Key]; id != 0 {
|
||||
byID[id] = append(byID[id], c)
|
||||
}
|
||||
}
|
||||
out := make([]Candidate, 0, len(cands))
|
||||
emitted := map[int]bool{}
|
||||
for _, c := range cands {
|
||||
id := seriesID[c.Key]
|
||||
if id == 0 {
|
||||
out = append(out, c)
|
||||
continue
|
||||
}
|
||||
if emitted[id] {
|
||||
continue // a later member of an already-emitted series
|
||||
}
|
||||
out = append(out, byID[id]...)
|
||||
emitted[id] = true
|
||||
}
|
||||
return out
|
||||
}
|
||||
136
backend/internal/terminology/series_test.go
Normal file
136
backend/internal/terminology/series_test.go
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
package terminology
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var zhSeries = SeriesParams{Enabled: true, HeadFinal: true}
|
||||
|
||||
func cand(key string, freq int) Candidate { return Candidate{Key: key, Src: key, Type: "term", Freq: freq} }
|
||||
|
||||
// TestDetectSeriesHeadAware pins the head-aware rule against the exact patterns the naive "differ in one
|
||||
// position" rule broke on (measured on BANK-FULL): a grade set clusters, but a set differing IN the head
|
||||
// does not (that is the type step's job), a shared head-rune across two families never forms a blob, and a
|
||||
// one-rune surface is never a member.
|
||||
func TestDetectSeriesHeadAware(t *testing.T) {
|
||||
cands := []Candidate{
|
||||
cand("甲等", 3), cand("乙等", 3), cand("丙等", 3), cand("丁等", 3), // grades of 等 → ONE series
|
||||
cand("元石", 40), cand("元海", 40), cand("元火", 5), // differ IN the head 石/海/火 → NOT a series (§2's job)
|
||||
cand("元气", 6), cand("酒气", 6), // share head 气 but only two → below min, no blob
|
||||
cand("转", 9), // one rune → never a member
|
||||
}
|
||||
got := DetectSeries(cands, zhSeries)
|
||||
|
||||
grade := []string{"甲等", "乙等", "丙等", "丁等"}
|
||||
id := got[grade[0]]
|
||||
if id == 0 {
|
||||
t.Fatalf("the grade set must form a series: %v", got)
|
||||
}
|
||||
for _, k := range grade {
|
||||
if got[k] != id {
|
||||
t.Fatalf("%s must join the grade series (id %d), got %d", k, id, got[k])
|
||||
}
|
||||
}
|
||||
// The §1↔§2 reconciliation: 元石/元海 differ in the HEAD, so they are different entities, not a series.
|
||||
for _, k := range []string{"元石", "元海", "元火", "元气", "酒气", "转"} {
|
||||
if got[k] != 0 {
|
||||
t.Fatalf("%s must NOT be a series member (id %d) — the naive rule's failure mode", k, got[k])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestDetectSeriesLongerHead covers a multi-rune shared head (甲等资质/乙等资质/丙等资质): the differing rune
|
||||
// is the leading modifier, the whole 等资质 tail is the head.
|
||||
func TestDetectSeriesLongerHead(t *testing.T) {
|
||||
cands := []Candidate{cand("甲等资质", 2), cand("乙等资质", 2), cand("丙等资质", 2)}
|
||||
got := DetectSeries(cands, zhSeries)
|
||||
if got["甲等资质"] == 0 || got["甲等资质"] != got["乙等资质"] || got["乙等资质"] != got["丙等资质"] {
|
||||
t.Fatalf("a set sharing the trailing head 等资质 must form one series: %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDetectSeriesRespectsPairData: the channel is off for a pair whose source is not dense-script — the same
|
||||
// single-position difference that clusters for zh must NOT cluster when Enabled is false (care/core is a
|
||||
// spelling coincidence), and the head side flips with HeadFinal.
|
||||
func TestDetectSeriesRespectsPairData(t *testing.T) {
|
||||
alpha := []Candidate{cand("care", 3), cand("core", 3), cand("cure", 3)}
|
||||
if got := DetectSeries(alpha, SeriesParams{Enabled: false, HeadFinal: true}); len(got) != 0 {
|
||||
t.Fatalf("series channel must be inert for a non-series pair, got %v", got)
|
||||
}
|
||||
// Head-INITIAL direction: the head is the leading rune, the modifier is non-initial. 등X where the tail
|
||||
// varies and 등 is shared as the head.
|
||||
headInit := []Candidate{cand("등가", 1), cand("등나", 1), cand("등다", 1)}
|
||||
got := DetectSeries(headInit, SeriesParams{Enabled: true, HeadFinal: false})
|
||||
if got["등가"] == 0 || got["등가"] != got["등나"] || got["등나"] != got["등다"] {
|
||||
t.Fatalf("with HeadFinal=false the leading rune is the head and the trailing modifier varies: %v", got)
|
||||
}
|
||||
// The same set under HeadFinal=true shares no head (they differ at the last position) → no series.
|
||||
if got := DetectSeries(headInit, zhSeries); len(got) != 0 {
|
||||
t.Fatalf("under head-final the trailing-varying set is not a series, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBatchKeepsSeriesWhole: a series scattered across key order lands in ONE batch even under a budget so
|
||||
// tight every singleton is its own batch — co-batching is the whole mechanism.
|
||||
func TestBatchKeepsSeriesWhole(t *testing.T) {
|
||||
// Key-sorted order interleaves the series with a non-member; a tiny budget would otherwise split it.
|
||||
cands := []Candidate{cand("丁等", 3), cand("丙等", 3), cand("乙等", 3), cand("甲等", 3), cand("中间", 50)}
|
||||
seriesID := DetectSeries(cands, zhSeries)
|
||||
batches := Batch(cands, 10, seriesID) // 10 runes: every unit overflows, so packing cannot help by luck
|
||||
var seriesBatch []Candidate
|
||||
for _, b := range batches {
|
||||
for _, c := range b {
|
||||
if seriesID[c.Key] != 0 {
|
||||
seriesBatch = b
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(seriesBatch) != 4 {
|
||||
t.Fatalf("all four grade members must share one batch, got %d: %v", len(seriesBatch), batches)
|
||||
}
|
||||
// Every candidate still appears exactly once across all batches.
|
||||
seen := map[string]int{}
|
||||
for _, b := range batches {
|
||||
for _, c := range b {
|
||||
seen[c.Key]++
|
||||
}
|
||||
}
|
||||
if len(seen) != len(cands) {
|
||||
t.Fatalf("batching dropped or duplicated a candidate: %v", seen)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBatchValueOrder: batches are processed most-frequent first, so a budget ceiling drops the rarest terms
|
||||
// (feed_cap) rather than the lexicographic tail. Content of each batch is untouched by the ordering.
|
||||
func TestBatchValueOrder(t *testing.T) {
|
||||
// Three singletons, each its own batch under a tiny budget; frequency is the only thing that differs.
|
||||
cands := []Candidate{cand("阿", 1), cand("布", 99), cand("此", 50)}
|
||||
batches := Batch(cands, 5, nil)
|
||||
if len(batches) != 3 {
|
||||
t.Fatalf("expected three singleton batches, got %d", len(batches))
|
||||
}
|
||||
if batches[0][0].Key != "布" || batches[len(batches)-1][0].Key != "阿" {
|
||||
t.Fatalf("batches must run most-frequent first, least-frequent last: %v", batches)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBatchNilSeriesMatchesLegacyOrder guards the alphabetic/off path: with no series and uniform frequency,
|
||||
// Batch preserves the incoming key order exactly (the pre-§1 behaviour), so a book that forms no series takes
|
||||
// a byte-identical path.
|
||||
func TestBatchNilSeriesMatchesLegacyOrder(t *testing.T) {
|
||||
var cands []Candidate
|
||||
for _, k := range []string{"a", "b", "c", "d", "e"} {
|
||||
cands = append(cands, Candidate{Key: k, Src: k, KWIC: []string{strings.Repeat("к", 200)}})
|
||||
}
|
||||
batches := Batch(cands, 400, nil)
|
||||
var order []string
|
||||
for _, b := range batches {
|
||||
for _, c := range b {
|
||||
order = append(order, c.Key)
|
||||
}
|
||||
}
|
||||
if strings.Join(order, "") != "abcde" {
|
||||
t.Fatalf("uniform-frequency, series-free batching must preserve key order, got %v", order)
|
||||
}
|
||||
}
|
||||
|
|
@ -914,31 +914,66 @@ func ParseReply(reply string, expected []string, normalize func(string) string,
|
|||
return out, st
|
||||
}
|
||||
|
||||
// Batch splits candidates into groups whose rendered size stays under maxRunes, preserving key order. A
|
||||
// single candidate larger than the budget still gets its own batch (never silently dropped).
|
||||
func Batch(cands []Candidate, maxRunes int) [][]Candidate {
|
||||
if maxRunes <= 0 || len(cands) == 0 {
|
||||
if len(cands) == 0 {
|
||||
return nil
|
||||
}
|
||||
// Batch splits candidates into groups whose rendered size stays under maxRunes. A series (a key present in
|
||||
// seriesID; pass nil to disable the channel) is kept WHOLE in one batch so the terminologist picks one
|
||||
// generic head for it (§1); a series or a single candidate larger than the budget still gets its own batch
|
||||
// rather than being split or silently dropped. The batches are then ordered by descending source frequency,
|
||||
// so a budget ceiling drops the least-frequent terms instead of the lexicographic tail (feed_cap, §7). Every
|
||||
// batch's CONTENT — hence its request hash and $0 resume — is independent of this ordering; only the call
|
||||
// order moves. Deterministic throughout.
|
||||
func Batch(cands []Candidate, maxRunes int, seriesID map[string]int) [][]Candidate {
|
||||
if len(cands) == 0 {
|
||||
return nil
|
||||
}
|
||||
if maxRunes <= 0 {
|
||||
return [][]Candidate{cands}
|
||||
}
|
||||
ordered := orderBySeries(cands, seriesID)
|
||||
var out [][]Candidate
|
||||
var cur []Candidate
|
||||
size := 0
|
||||
for _, c := range cands {
|
||||
// +2 for the blank line RenderBatch puts BETWEEN blocks: without it the accountant counts something
|
||||
// the renderer does not emit, and a 200-term book overruns the configured budget by 2×(N−1) runes.
|
||||
n := len([]rune(RenderBatch([]Candidate{c}))) + 2
|
||||
if len(cur) > 0 && size+n > maxRunes {
|
||||
flush := func() {
|
||||
if len(cur) > 0 {
|
||||
out = append(out, cur)
|
||||
cur, size = nil, 0
|
||||
}
|
||||
cur = append(cur, c)
|
||||
size += n
|
||||
}
|
||||
if len(cur) > 0 {
|
||||
out = append(out, cur)
|
||||
for i := 0; i < len(ordered); {
|
||||
j := i + 1 // extend over a whole series block; a singleton is a unit of one
|
||||
if id := seriesID[ordered[i].Key]; id != 0 {
|
||||
for j < len(ordered) && seriesID[ordered[j].Key] == id {
|
||||
j++
|
||||
}
|
||||
}
|
||||
unit := ordered[i:j]
|
||||
if n := unitRunes(unit); len(cur) > 0 && size+n > maxRunes {
|
||||
flush()
|
||||
cur, size = append(cur, unit...), n
|
||||
} else {
|
||||
cur, size = append(cur, unit...), size+n
|
||||
}
|
||||
i = j
|
||||
}
|
||||
flush()
|
||||
sort.SliceStable(out, func(a, b int) bool { return batchFreq(out[a]) > batchFreq(out[b]) })
|
||||
return out
|
||||
}
|
||||
|
||||
// unitRunes is the rendered size the accountant charges a unit: each member's single-block render plus the
|
||||
// blank line RenderBatch puts between blocks (+2). Without the +2 a 200-term book overruns its budget by
|
||||
// 2×(N−1) runes. Summing per member matches the original per-candidate arithmetic exactly.
|
||||
func unitRunes(unit []Candidate) int {
|
||||
n := 0
|
||||
for _, c := range unit {
|
||||
n += len([]rune(RenderBatch([]Candidate{c}))) + 2
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func batchFreq(b []Candidate) int {
|
||||
n := 0
|
||||
for _, c := range b {
|
||||
n += c.Freq
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
|
|
|||
|
|
@ -354,7 +354,7 @@ func TestParseReplyKeepsAWholeRenderingAndBatchBudgetHolds(t *testing.T) {
|
|||
for i := range cands {
|
||||
cands[i] = Candidate{Key: "k", Src: "s", Type: "term"}
|
||||
}
|
||||
for i, b := range Batch(cands, 600) {
|
||||
for i, b := range Batch(cands, 600, nil) {
|
||||
if n := len([]rune(RenderBatch(b))); n > 600 {
|
||||
t.Fatalf("batch %d renders %d runes over a 600-rune budget", i, n)
|
||||
}
|
||||
|
|
@ -409,7 +409,7 @@ func TestBatchCoversEveryCandidateExactlyOnce(t *testing.T) {
|
|||
for _, k := range []string{"a", "b", "c", "d", "e"} {
|
||||
cands = append(cands, Candidate{Key: k, Src: k, KWIC: []string{strings.Repeat("к", 200)}})
|
||||
}
|
||||
batches := Batch(cands, 400)
|
||||
batches := Batch(cands, 400, nil)
|
||||
if len(batches) < 2 {
|
||||
t.Fatalf("the fixture must actually split, got %d batch(es)", len(batches))
|
||||
}
|
||||
|
|
@ -429,7 +429,7 @@ func TestBatchCoversEveryCandidateExactlyOnce(t *testing.T) {
|
|||
}
|
||||
// A single oversized candidate is never silently dropped.
|
||||
big := []Candidate{{Key: "big", Src: "big", KWIC: []string{strings.Repeat("ю", 5000)}}}
|
||||
if got := Batch(big, 10); len(got) != 1 || len(got[0]) != 1 {
|
||||
if got := Batch(big, 10, nil); len(got) != 1 || len(got[0]) != 1 {
|
||||
t.Fatalf("an oversized candidate must still get its own batch, got %v", got)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
43
backend/prompts/zh-ru/classifier.md
Normal file
43
backend/prompts/zh-ru/classifier.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<!-- v1 (01.08, bank-quality §2): фокус-классификатор типа. Файл — ДАННЫЕ: определения классов и примеры
|
||||
пары zh→ru живут здесь, движок хранит лишь закрытый список идентификаторов name/place/title/term и в
|
||||
снапшот этот текст не фолдит. Обоснование — D39.65 (тип черновика неверен 12–22%, branch-harm 元石→юаньши)
|
||||
и §3 отчёта POLYGON_BANK_PROBES (отдельный фокус-проход чинит 6/6). -->
|
||||
|
||||
Ты — классификатор терминов издательского перевода с языка «{{source_lang}}» на «{{target_lang}}».
|
||||
Книга: «{{title}}». Жанр: {{genre}}. Аудитория: {{audience}}.
|
||||
|
||||
Твоя задача — НЕ переводить. По каждому термину книги реши, к какому из ЧЕТЫРЁХ классов он относится.
|
||||
Класс определяет, как термин будет переведён дальше: имена и топонимы ТРАНСКРИБИРУЮТСЯ, а титулы и
|
||||
реалии переводятся ПО СМЫСЛУ. Ошибка класса — это реалия, ошибочно записанная как имя, которую потом
|
||||
транслитерируют в бессмысленный слог (например 元石, «первородный камень», стал бы «юаньши»). Поэтому
|
||||
решай по КОНТЕКСТУ, а не по строке `type:` — она черновая и может быть неверна.
|
||||
|
||||
Классы (ответ давай ровно этим английским словом):
|
||||
- `name` — имя собственное персонажа или существа: 方源 (Фан Юань), 青鸟 как кличка. Транскрибируется.
|
||||
- `place` — топоним: гора, город, секта как место, дворец. 青茅山 (гора Цинмао). Транскрибируется.
|
||||
- `title` — титул, звание, ранг, ДОЛЖНОСТЬ, а также название произведения внутри книги (стихи, песни,
|
||||
трактаты, приёмы-техники как ИМЕНОВАННЫЕ произведения): 家老 (старейшина), 甲等 (ранг). Переводится по смыслу.
|
||||
- `term` — реалия, понятие, предмет, материал, вещество, класс существ — всё, что переводится ПО СМЫСЛУ,
|
||||
а не транслитерацией: 元石 (первородный камень), 蛊 (гу — класс существ), 灵泉 (духовный источник),
|
||||
窍 (апертура). Если сомневаешься между `name`/`place` и `term`, а термин обозначает ВЕЩЬ или ПОНЯТИЕ,
|
||||
а не конкретное лицо или место, выбирай `term`.
|
||||
|
||||
Как решать:
|
||||
- Опирайся на строки `ctx:` — они показывают, чем термин является в тексте. Одна и та же морфема бывает
|
||||
и частью имени, и реалией; контекст различает их.
|
||||
- Строка `type:` — черновая догадка, которую ты и проверяешь. Не повторяй её механически.
|
||||
- Не выдумывай новых классов и не отвечай ничем, кроме одного из четырёх слов выше.
|
||||
|
||||
Формат ответа — по одной строке на термин, ровно два поля, разделённые СИМВОЛОМ ТАБУЛЯЦИИ:
|
||||
первое поле — термин исходника, второе — класс (name | place | title | term).
|
||||
|
||||
Пример строки ответа (символ между полями — настоящая табуляция):
|
||||
|
||||
元石 term
|
||||
|
||||
Никаких заголовков, нумерации, комментариев и markdown. Термины, которых нет в списке, не добавляй.
|
||||
|
||||
---USER---
|
||||
Термины книги:
|
||||
|
||||
{{text}}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
# Журнал прогресса
|
||||
|
||||
> **⟶ ТЕКУЩЕЕ СОСТОЯНИЕ** (на 2026-08-01, голова D39.68). **Источник истины по РЕШЕНИЯМ — `architecture/05-decisions-log.md` (D1–D39.68); этот файл — ЖУРНАЛ.**
|
||||
> **⟶ ТЕКУЩЕЕ СОСТОЯНИЕ** (на 2026-08-02, голова D39.69). **Источник истины по РЕШЕНИЯМ — `architecture/05-decisions-log.md` (D1–D39.69); этот файл — ЖУРНАЛ.**
|
||||
> - **Сделано (сводно; детали — D-лог и архив-слайсы):** Ф0 ✅ · Ф1-инфра ✅ · арх-ресет D39 (7 слоёв, паки 11–16) ✅ · паки 17 «канал B» · 18 «долги» · 19 «голос/состояние» · 20 «банк+терминолог» ✅ (D39.26–28/31/41–56) · мини-прогон (D39.37) и ХОЛОДНЫЙ прогон (D39.58: recall банка 0.918/0.980, банк приходит переведённым) приняты · полигон-пакеты 5–8, ToS-речек (D39.57), Р6+P4 (D39.61) закрыты · **карта языковой привязки движка (D39.60): 149 сайтов/45 файлов, книго-ось чиста (4), ja→ru безопасно без правки Go, en→ru — нет** · Р1–Р4 закрыты целиком (D39.63: Р2 = ЗНАЧЕНИЕ, Р4 = норма потолка recall) · **ФАЗА 2 ОБЩНОСТИ ✅ (D39.64: П0 эмбед-хеш · П1 цель-шов · П2 скрипт-шов, ko-баг закрыт · П3 нарезка · П4 манифест-по-каналам; голден вердикт-нейтрален 0/172, майнер-парити EXACT)** · жанровый словарь отменён как класс (D39.47) · петля ремонта построена и НЕ включена (D39.38).
|
||||
> - **Курс (D39.59–67): ОБЩНОСТЬ ✅ → КАЧЕСТВО БАНКА (сейчас; термин владельца D39.67, бывш. «автономность»: банк обязан приходить ВЕРНЫМ без ручной правки, подпись опциональна).** Обе ветки параллели ЗАКРЫТЫ: фаза 2 общности (D39.64; 9 флагов §5 — строка 14) и полигон-сырьё+пробы банка (D39.65). **Текущее: ДВА бэкенд-промта ВЫДАНЫ (D39.68, хендофф владельца — дизайн у бэкендеров): банк-качество (этап 1 ресёрч+дизайн, read-only) ∥ пакет-чекеров (стройка) — обе кучки $0, платные прогоны стоят на развилке 74. Оба промта несут глобальный критерий + мандат ревизии посылок.** ⚠ **DeepSeek-V4-Flash-0731: веса сменились под слагом — боевая черновая форма покупает пустые ответы (бьёт и терминолога); платные прогоны СТОП до ре-пробы (строка 74; оперативно «(в) ждём», D39.63). Перекупок на этой фазе нет — стендовый корпус одноразовый (D39.63).**
|
||||
> - **Горизонт (D39.62 «планировать подальше», хирургия D39.67):** дизайн-пак КАЧЕСТВА банка (серия-батчинг · тип-пере-вывод · Decl-шов/27 · 36б · веб-фетч пере-замер · вопрос резюме-слоя 80) → СТРОЙКА банка ∥ пакет-чекеров (25, $0) → ре-проба flash (74) → **ДОБОР ИДЕАЛА** (первым прогоном влиты бывш. оси мини-прогона: голос 24 · авто-режим · итерация №2 редакторов 65 · полная цена 16; плюс свип гипотез качества: веса K1–K12 13а · стенд пакета-6 · вне-претрейн чекпоинт 55) → ВТОРАЯ ПАРА живьём (ja→ru; гейтит П5-майнер и §0 фактом; преп 81) → МАСШТАБ (7,78 млн симв., ~2284 раздела) → пилот Ф2.5 (судья; строки 62–68, 85) → Ф3 ридер-IDE (строки 69–71). **Стоячие:** ToS-триггер 25.10 · Ш-2 трипваер до go1.27 · строка 74 перед любым платным прогоном.
|
||||
> - **Курс (D39.59–69): ОБЩНОСТЬ ✅ → КАЧЕСТВО БАНКА ✅ (ядро построено).** Банк-пак принят целиком (D39.69: серия-батчинг head-aware · тип-классификатор первичным · Decl-стеммер+neuter; 36а/36в/27/84/feed_cap закрыты; голден вердикт-нейтрален, «4 серии на живом BANK-FULL» воспроизведено независимо). **Текущее: ПАКЕТ-ЧЕКЕРОВ (промт `BACKEND_CHECKER_PACKAGE_SESSION_PROMPT.md` выдан, сессия НЕ запущена) + фикс-лист банка (строка 87, несущее — «ь» в decl_suffix) + 8 вопросов владельцу (строка 88, вкл. ⚠Q8 книжные каноны в classifier.md).** ⚠ **DeepSeek-V4-Flash-0731: платные прогоны СТОП до ре-пробы (строка 74; при ре-пробе добрать платный порог §2 «6/6 harm»). Перекупок нет — стендовый корпус одноразовый (D39.63).**
|
||||
> - **Горизонт (D39.62/67):** пакет-чекеров (строка 25) + фикс-лист банка (87) → ре-проба flash (74; + платный порог 6/6 классификатора + пробы 36б-замера Q2/эмиссии Q7 по слову) → **ДОБОР ИДЕАЛА** (первым прогоном: оси голоса 24 · авто-режим · итерация №2 редакторов 65 · цена 16 · веса K1–K12 13а · вне-претрейн чекпоинт 55) → ВТОРАЯ ПАРА живьём (ja→ru; преп 81) → МАСШТАБ → пилот Ф2.5 (гейт резюме-строки 80; строки 62–68, 85) → Ф3 ридер-IDE (69–71). **Стоячие:** ToS-триггер 25.10 · Ш-2 до go1.27 · строка 74 перед любым платным прогоном.
|
||||
> - **Стек:** draft deepseek-v4-flash thinking-ON (+банкнота) **⚠0731** → терминолог (та же модель, батчи, экран `target_script`) → editor deepseek-v4-pro БИЛИНГВ ИНТЕРИМ (вендором НЕ тронут; glm-5 резерв) → судья gemini (Ф2, полигон); канал B Mistral+grok; ~$0.85/ранобэ (D30.4) — пере-калибровка после развилки 74.
|
||||
> - **ЕДИНЫЙ БЭКЛОГ — секция «Бэклог» ниже** (одна таблица, единственный трекер; каждая петля обязана иметь диспозицию: решено / отложено-с-записью / отклонено; ведёт оркестратор).
|
||||
> - Архивы хроники: `archive/PROGRESS-2026-07-04-10.md` (D31) · `-10-13` (D39.6-гигиена) · `-13-25` (стройка паков 11–16, rerun2) · **`-25-31` (паки 17–20 · мини-прогон · полигон-пакеты 5–8 · ToS · холодный прогон; D39.26–58)**. Записи ниже — живой хвост (эра D39.59+).
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
| **— ВИСИТ НА ВЛАДЕЛЬЦЕ —** | | | | | |
|
||||
| 2 | Четыре вопроса границы «алгоритмического идеала» (Annotator · семантический судья · ja→ru §B5 · ru-target до/после) | владелец | скоро (гейт «добора идеала» перед МАСШТАБОМ) | отдельное решение | D39.33, D39.38 |
|
||||
| 4 | Ре-чек ToS по триггерам — первый ИСПОЛНЕН 31.07, дельт вердиктов нет (D39.57); предмет Google-мониторинга перевешен на цепочку договора (PUP · Additional ToS · Google APIs ToS); следующие: квартал 25.10.2026 · первая лейблованная книга · любая правка `accepts_labels` | владелец/полигон | скоро (следующий триггер 25.10.2026) | отдельное решение (правило) | D39.32, D39.57 |
|
||||
| 5 | Судья-с-декоем — условие «после холодного старта» НАСТУПИЛО (D39.58); форма возврата = рецензент второго мнения по банку, несогласия колонкой подписной таблицы (строка 36б) | владелец | скоро | дизайн 36б | D39.50 п.6, D39.51 п.4, D39.58 |
|
||||
| 5 | Судья-с-декоем/36б «второе мнение» — бэкенд-стройка ЗАКРЫТА дизайном (D39.69): маршрут = полигон-ЗАМЕР нужности рецензента-другого-семейства ПОВЕРХ §1+§2 (Q2); переживёт замер → тонкая колонка наблюдаемости, не гейт | владелец → полигон | когда-нибудь (по слову, можно при ре-пробе 74) | полигон-замер → решение | D39.50 п.6, D39.58, D39.69 |
|
||||
| 6 | Строка Gemini в `accepts_labels` — ждёт слова владельца о подписи интерпретации сужения как разрешения; ре-чек 31.07 понизил приоритет: подпись даёт ПРАВО, но не судью 18+ (фильтр неконфигурируем D22.6 + обход запрещён договором) — возвращаться при вопросе, требующем именно права | владелец | когда-нибудь | отдельное решение | D39.32, D39.57 |
|
||||
| 7 | Q2 «редактор под `sexually-explicit`» (grok-4.3 интерим · glm-5 после сверки ToS Z.AI · mistral вон) | владелец | когда-нибудь (перед первой explicit-книгой) | отдельное решение | D39.27 п.7 |
|
||||
| 8 | D22.7 пер-чанковый L3-скрин — неснятое предусловие первой erotica-книги в проде, отдельный гейт | владелец | когда-нибудь | отдельное решение | D22.7, CURRENT-STATE (оркестратор) |
|
||||
|
|
@ -29,26 +29,25 @@
|
|||
| 13а | Веса рубрики K1–K12 не выбраны (Q1: «по данным фазы B»; фаза B прошла на BWS без весов — выбор нужен к агрегатной приёмке качества книги) | владелец | когда-нибудь (к пилоту) | Ф2.5 / отдельное решение | D39.44 Q1, D39.46 |
|
||||
| 13б | `speech-cue.txt` zh НЕ шипить до замера голос-флаггера на холодном прогоне (данные для недоказанного детектора = инерция; вывод обсуждения 31.07): оси пустые → файл не нужен, оси ловят → решение владельца о шипе (байты двигают `LangpackVersion`) | владелец | отложено-до-замера | первый прогон добора идеала (оси голоса, строка 24) → отдельное решение | D39.56, PACK19_BUILD §7.2, чат 31.07 |
|
||||
| 14 | Четыре data-инженерных флага фазы-2 общности, каждый с рекомендованным дизайном (§5 отчёта): ё-фолд → target-данные (`char_fold`; смена сигнатуры core-примитива через вердикт-несущий банк, сдвиг `memory_version`) · segmentation-фолбэк громким (ломает контракт corridor-фолбэка `pipeline.go:839-848` + голден-фикстура на нём; лечение — объявить блок в голден-yaml) · `Fertility` per-script map (калибровка ждёт боевого токенизатора) · `source-encoding.txt` + Go-реестр декодеров | владелец | когда-нибудь (следующий пак общности) | отдельное решение | D39.64, GENERALITY_PHASE2 §5.1–5.4 |
|
||||
| 86 | Мелкие открытые решения одним списком (свип D39.66): echo-семантика дропнутого c-lite-члена (D39.18 — оставить pre-c-lite?) · magnitude-кап тяжёлой CJK-утечки (D38.3 п.4в; сейчас strip+flag) · feed_cap терминолога (D39.45; предметно пере-смотрит дизайн-пак 73) | владелец | когда-нибудь | отдельные решения / дизайн-пак 73 | D39.66 |
|
||||
| 86 | Мелкие открытые решения одним списком (свип D39.66): echo-семантика дропнутого c-lite-члена (D39.18 — оставить pre-c-lite?) · magnitude-кап тяжёлой CJK-утечки (D38.3 п.4в; сейчас strip+flag) | владелец | когда-нибудь | отдельные решения | D39.66, D39.69(feed_cap закрыт §1) |
|
||||
| **— ХВОСТЫ ХОЛОДНОГО ПРОГОНА (исполнен достаточно, D39.58; Р7 владельца) —** | | | | | |
|
||||
| 16 | Полная цена холодного старта: edit-волна не гонялась (Р7); измерено всё ДО редактуры (драфт+банкнота $0.0247/волна · терминолог $0.006–0.013/проход) | бэкенд | когда-нибудь (первый прогон добора идеала) | добор идеала | D39.58 |
|
||||
| 18 | `蛊` не приходит НИ ОДНИМ каналом (модель объявляет составные, майнер режет по длине) — структурная дыра эмиссии на голове частотного понятия; доставка подписанной короткой строки проверена $0-пином; третий сайт того же порога — алиас-канал `miner_alias.go:44,93` (D39.60) | бэкенд | когда-нибудь | отдельное решение (рядом с 36а) | D39.58, D39.60 |
|
||||
| 18 | Эмиссия головы 蛊: сид-половина = чек-лист (определяющий терм книги сидится `allow_short`, D39.50); эмиссия-КАНАЛ демотирован до ИЗМЕР-ГЕЙТА (Q7): полигон-замер «меняет ли эмиссия головы рендер композитов» — строить ТОЛЬКО по эффекту и ПОСЛЕ #10 (однознаковый пост-чек чинится чекер-паком); перед стройкой резолвить K-гейт и KWIC-грязь одиночной головы | полигон → бэкенд | когда-нибудь (после #10, по слову Q7) | полигон-замер → решение | D39.58, D39.60, D39.69 |
|
||||
| 24 | Остаток: авто-режим флага (проект Б не гонялся) · оси голоса A–D (профили не подписаны; черновик готов — `coldrun-a/SIGN-PACKAGE.md` §3) ; + слот инъекции голоса и решение gates.voice при ненулевых осях (D39.55/D21 п.2) · слой-2 voice не строим до доказательства слоя-1 · сверить until_ch 白凝冰 при курации сида (D19.3в) | бэкенд | когда-нибудь (первый прогон добора идеала) | добор идеала | D39.40, D39.58 |
|
||||
| **— ТЕКУЩАЯ ОЧЕРЕДЬ (D39.59) —** | | | | | |
|
||||
| 73 | Арка КАЧЕСТВА банка (термин D39.67) — ВХОД СОБРАН (D39.65: 21/22 закрыты, 36а/36в с проверенными фиксами, Decl 142/142 null, веса §C2-3 не тюнить — рычаг в контекстах/серии): дизайн-пак оркестратора = серия-батчинг по морфеме · тип-пере-вывод · Decl-шов (терминолог эмитит формы / морфопроход) · 36б второе мнение · Р3-остаток (сокращённая форма, строка 27) · пере-замер веб-фетча под АВТОНОМНЫМ критерием; заметка: first-occurrence-тест KWIC-консолидатора — кандидат $0-захода ; + кандидаты свипа D39.66: ranked-set: тип записи + порядок ступеней (инъективность dst УЖЕ есть — `InjectivityCollisions` warning-only; решить hard-режим для ranked-set) · STITCH-ретирование инъекции · tmctl «замена термина с главы N» · эмбеддинг-второй-эшелон A7/D2 (дать диспозицию) · feed_cap (D39.45) · вопрос резюме-слоя (строка 80) | бэкенд | в работе (промт выдан D39.68: этап 1 дизайн → СТОП → ратификация → этап 2 стройка) | `BACKEND_BANK_QUALITY_SESSION_PROMPT.md` | D39.59, D39.60, D39.65 |
|
||||
| 74 | ⚠ **DeepSeek-V4-Flash-0731: боевая черновая форма покупает пустые ответы — блокер ПЛАТНЫХ прогонов.** Оперативное состояние = «(в) ждать» (D39.63; фаза-2 всё равно $0-код). Перед следующим платным прогоном — ре-проба flash за копейки → если не устаканилось, выбор: (а) флор 16000 (сдвиг request_hash безболезнен — перекупок на этой фазе нет; хвост не закрывает, упор 2/5) или (б) слать `reasoning_effort` явно (вендорская модель, дешевле ~22%; но амендмент `echoMineViolation`, эхо ~6% и качество черновика при `low` не судилось — нужен замер ~$0.05; тогда же предмет для постинструкции/префилла); включение ручки #77 (ре-ген эха перед эскалацией, построена D39.64, дефолт 0) — тем же решением; при включении помнить общий счётчик attempt с length-ре-геном + рост `maxTokensForAttempt`; остаток §12.7 (8 несудимых fidelity-пар ≈$0.10 · ja-вход ≈$0.005 · prefill-continuation research/21 §1.17) — вернуться при предмете wiring; ⚠ cap 8000 бьёт и СТАДИЮ ТЕРМИНОЛОГА (reasoning 8063/батч, D39.65) — резолюция обязана накрыть терминолога | владелец (по ре-пробе) | перед следующим платным прогоном | ре-проба → решение | D39.61, D39.63, D39.64, POLYGON_PROMPTLANG_ANTIECHO §12.4 |
|
||||
| 78 | Леджер денег = НИЖНЯЯ граница: «2xx body decode failed (call IS billed)» — провайдер списал, исходная попытка в `request_log` НЕ попадает (≤$0.009/сессия); дизайн фикса ГОТОВ (GENERALITY_PHASE2 §5.6: `BilledDecodeFails` + per-billed-attempt settle с `Estimated=true`), но это money/ledger-путь — не $0-хастл | бэкенд | когда-нибудь (money-паком) | отдельное решение (дизайн §5.6) | D39.61, D39.64 |
|
||||
| 79 | Остаточные Cyrillic-хардкоды пар-гейтед DC-чекеров: `checks/repair.go:131` (трим DC1-спана) · `checks/checkers.go:457,474` (`isCyrLetter` словограница register) — инертны без dc-данных пары, но пара С DC-чекерами и не-ru целью потребует правки Go; кандидат на `c.wordScript` ; + lintNumberMagnitude гейтить по объявленному письму источника (находка §8 карты D39.60) | бэкенд | когда-нибудь (следующий пак цель-оси) | ближайшее касание чекеров | D39.64 |
|
||||
| 87 | Фикс-лист банк-пака (D39.69, приёмка): **«ь» (и класс -нь/-ль) в `decl_suffix` — приёмочный кейс «Фан Юаню»→«Фан Юань» сейчас ПРОВАЛИВАЕТСЯ (одна строка данных + пере-захват голдена)** · комментарий `Batch` «request hash independent of ordering» неверен (ChunkIdx фолдится — поправить) · оверсайз-серия одним батчем (помнить мину cap-8000 терминолога) · classify_types при выключенном terminology-гейте = тихий no-op + config-тесты classify-веток · мутационный тест neuter-фолда · voice-checker без стеммера (решить: заводить или объявить не-нужным) · дистилл-фикстура BANK-FULL (пин «4 серии») | бэкенд | скоро (малой пачкой при следующем касании банка; можно паровозом к чекер-паку по слову владельца) | ближайшее касание | D39.69 |
|
||||
| 88 | Вопросы дизайн-пака банка §9 у владельца: Q1 канон-пин серий между прогонами (реком: не сейчас) · Q2 полигон-замер 36б (реком: да, до стройки) · Q3 подтвердить резюме-гейт Ф2.5 · Q4 hard-инъективность ranked-set (реком: нет) · Q5 вкус авто-транслита реалий (юаньци/цзин-ци-шэнь — промпт-инструкция?) · Q6 «сокращённая форма» = морфоклип или прозвище + компат neuter-сидов · Q7 замер эмиссии головы 蛊 · **⚠Q8 книжные каноны в `classifier.md` (方源 и др.) — санкция или жанрово-генерические примеры** | владелец | скоро | ответы → мелкие правки | D39.69, BANK_QUALITY_DESIGN §9 |
|
||||
| **— НАХОДКИ СВИПА ПОЛНОТЫ (D39.66: 951 обязательство проверено, потери возвращены в трекер) —** | | | | | |
|
||||
| 80 | Резюме-слой памяти (глава→арка→книга) + fact-gate (R9/Q6, D25 п.8–9; «вторая половина банка», реестр-06 D1 «первоклассный пробел») — дизайн-пак 73 ОБЯЗАН дать диспозицию: строить в Ф2.5 или закрыть с обоснованием «KWIC-банк+reveal-окна достаточны» | бэкенд/владелец | скоро (вопросом в дизайн-пак 73) | дизайн-пак 73 → отдельное решение | D25 п.8–9, D30.8, 06-реестр D1, D39.66 |
|
||||
| 80 | Резюме-слой памяти — проза-суммарайзер ЗАКРЫТ (D39.69); строка = ГЕЙТ ПИЛОТА Ф2.5: первый деливерабл пилота — «допускает ли автономная нарратив-состояние-строка ДЕТЕРМИНИРОВАННЫЙ верификатор (source-anchored) — или это D1-компаундинг со схемой»; не-покрытые классы (source-anchored reveal · арк-колбэки без ключа) реальны, но не измерены как дефект | владелец (Q3) → Ф2.5 | когда-нибудь (пилот) | Ф2.5 пре-рег | D25 п.8–9, D39.66, D39.69 |
|
||||
| 81 | ja-преп B6-ja (сужено пост-сверкой D39.66): Поливанов-валидатор (`translit_policy` = «Phase 2»-заглушка, migrate.go:186/194) + kana-омограф POS/known-word гейтинг / B6-токенизатор (memory.go:1048); чек-лист kana-алиасов сида УЖЕ построен (backend/README §ja + `AttachRubyAliasesToManual`) — остаток в нём только дизамбигуация двойных чтений (тот же B6) | бэкенд | когда-нибудь (перед ja→ru) | ja→ru-пак (рядом 35) | D16.4, D17.1, D18, D39.66 |
|
||||
| 82 | Морфо-гейт РОДА (C3: русский глагол прош. вр. при gender=hidden = механический спойлер-канал; «жалоба №1 читателей MTL»; python-сайдкар/pymorphy, связка с морфопроходом Decl из 73) | бэкенд | когда-нибудь (Ф2-гейты) | отдельный пак (с 52) | 06-реестр C3, research/12, D39.66 |
|
||||
| 83 | F4: бэкап + integrity_check SQLite-файла книги перед платным прогоном (VACUUM INTO; SPOF — тихая потеря банка и подписей владельца) | бэкенд | когда-нибудь (до МАСШТАБА) | малый пак (с 67/78) | 06-реестр F4, D39.66 |
|
||||
| 84 | Ратифицированный канон D39.21 «generic „гу“ = СРЕДНИЙ род» без носителя реализации: gender-enum neuter (сдвиг memory_version) + сид-дельта, либо явное закрытие «decl.invariant достаточно» ; пост-сверка: `gender: neuter` в сиде сегодня = ТИХИЙ no-op (memseed без валидации рода) — при стройке добавить seed-lint; «decl.invariant достаточно» закрывает только несклоняемость, не согласование | бэкенд/владелец | когда-нибудь (со следующей курацией сида) | отдельное решение | D39.21, D39.66 |
|
||||
| 85 | Пилот-преп добор (к переупаковке PACKAGE4): Bertalign GOLD-пары + zh→ru spot-check (D29.2в; GPL-тулинг отдельно) · fidelity-каскад R2 shadow с корпус-подготовкой (D25.2) · exp08 v3 пере-съём COGS до фиксации budget_usd (D21.8) | полигон | когда-нибудь (Ф2.5) | пилот-преп-промт | D25, D29, D21.8, D39.66 |
|
||||
| **— ПАК-21 «чекеры»: РАСТВОРЁН (D39.62); шестёрка исполнена 2/6 фазой 2 (D39.64: #11 · строка 26), остаток — ниже —** | | | | | |
|
||||
| 25 | Остаток дефектов чекеров (4 позиции, флаг §5.5 фазы-2): атрибуция K4b · inner_marker-вето-на-тире · приёмка гомоглиф-детектора (функционально ЕСТЬ — `sanitizer.go:431` `tk.mixed`) · строгий OffLanguage (оговорка 36а) — K-рубрика, корректность только против размеченного корпуса `~/books/gu-zhenren/labels/` (на стенде ЕСТЬ; харнесс замера утрачен в scratch — пересобрать) ПОЛНЫЙ ОСТАТОК (свип D39.66): корзина II целиком — #3 chevron-речь · #5 K5a-предохранитель (数-магнитуды) · #6 заглавная латиница/маркеры TM-BANK · #7 порог-3 (судьбу решить по labels + Р4-контракт) · #10 однознаковые подстрочные ключи (转) — плюс #1 K5c-супрессор пер-вхождением + имплементация Р2-контракта в детекторах (D39.63) · #8 两→cheng_re (данные, батчем) · резидуал R4 пака-11 (DC7 · omission-бэкстоп · DC2 word↔word) · Problem-классы research/22 §2 №5 · alignment-защита авторских рефренов (04-unhappy §7) | бэкенд | в работе (промт выдан D39.68; свежий прогон НЕ нужен, labels/ статичен) | `BACKEND_CHECKER_PACKAGE_SESSION_PROMPT.md` | D39.62, D39.64, GENERALITY_PHASE2 §5.5 |
|
||||
| 27 | Пост-чек обязан принимать сокращённую форму терма как поле записи (Р3/Q4) | бэкенд | скоро | строка 73 (арка банка, вместе с Decl-швом) | D39.44 Q4, D39.62 |
|
||||
| 28 | Банк-линт латиницы в dst или строгая форма языкового предиката (7 строк утечки алфавита проходят экран) | бэкенд | скоро | пакет-чекеров при свежем мини-прогоне (флаг §5.5; помнить оговорку 36а: «甲等 → класс Цзя» строгой формой не ловится) | D39.52, D39.62, D39.64 |
|
||||
| 28а | Prompt-injection-проба входного текста ($0): сепаратор ⟦TM-BANK-v1⟧ и якорь-подобные маркеры В ТЕКСТЕ КНИГИ — поведение среза/парсера/инъекции (книга = недоверенные данные; инструментов у моделей нет, но канал банкноты читает вывод по маркеру) | бэкенд/полигон | когда-нибудь | малая проба | сводка-ревью 26.07 |
|
||||
| **— СВИП ГИПОТЕЗ —** | | | | | |
|
||||
|
|
@ -61,9 +60,7 @@
|
|||
| 34а | Экран кодировки не видит `<meta charset>` без XML-объявления (епаб с gb18030-метой прочтётся как UTF-8) — до-паковая дыра, сознательно не тронута пачкой 14а (паритет со старым ридером) | бэкенд | когда-нибудь | отдельное решение | D39.54, SMALLPACK §8 |
|
||||
| 35 | Не-CJK майнер/банк: en-детектора не существует (hanRuns=0), ja с zh-таблицами активно неверен (0/10); нужен второй ДЕТЕКТОР (прототип `mine_nonhan.py`) + G1–G10 требования generic-майнера (вкл. квадратичность G9 ≈4.6 ч) | бэкенд | когда-нибудь | отдельный пак generic-майнера (с ja→ru) | D39.37(8), D39.43, D39.50 п.8, POLYGON_PREMEASURE §6 |
|
||||
| 36 | Кластеризация майнера глотает родовые титулы (族长/学堂家老 в кластере 葛家) — закрыто на границе сборки входа, сама кластеризация не чинилась | бэкенд | когда-нибудь | отдельное решение | D39.43, D39.45 |
|
||||
| 36а | Валидация поля `type` терма — ПРОМЕРЕНО (D39.65): ненадёжен 12–22%, place ~61%, 2 строгих branch-harm 元石/元海; фикс проверен живьём — сфокусированный классификатор той же моделью чинит 6/6 harm-направления; дизайн-кандидаты: тип-эмиссия терминологом из контекста + $0-экран «name/place с многословным строчным dst» | бэкенд | скоро (вход дизайн-пака 73) | дизайн-пак арки банка | D39.58, D39.65 |
|
||||
| 36б | Второе мнение по банку: рецензент ДРУГОЙ моделью, несогласия — отдельной колонкой подписной таблицы (судья-с-декоем в роли рецензента; решает владелец, D39.46-ограничение не бьёт) | бэкенд/полигон | когда-нибудь | отдельный дизайн (Р4) | D39.58, D39.51 п.4 |
|
||||
| 36в | Серийная консистентность — ПРИЧИННО показана граница батча (甲等«класс»/батч-5 vs 乙丙丁等«ранг»/батч-0) и фикс живьём (D39.65): со-батчинг серии = согласованно+детерминированно, серий-зависим (转 no-op); реализация без пар-литералов — кластер по общей исходной морфеме; канон-пин родового слова между книгами — отдельный дизайн | бэкенд | скоро (вход дизайн-пака 73) | дизайн-пак арки банка | D39.58, D39.65 |
|
||||
| 36г | Проводной формат каналов пересказан прозой в каждом пар-промпте, парсер в Go, связи нет — формат в шаблон движка (`{{banknote_format}}`); линты держат класс, дублирование остаётся | бэкенд | когда-нибудь (со следующей правкой промптов) | отдельное решение (Р5) | D39.58 |
|
||||
| 37 | Квадратичности: `AttachKWIC` (16.8 с / 2000 кандидатов; фикс — многошаблонный поиск) + src-правило банкноты при промахе чанк-пути (строка × длина книги; на 10 главах не видна) | бэкенд | когда-нибудь | отдельное решение | D39.45, PACK20_BANK_BUILD §8.5.1, D39.58 |
|
||||
| 38 | Смета не видит пере-покупку прохода терминолога на каждой итерации подписи (роль не пишет `chunk_status`, не видна `projectRebill`) | бэкенд | когда-нибудь | отдельное решение | D39.45, PACK20_BANK_BUILD §8.5.2 |
|
||||
|
|
@ -106,6 +103,10 @@
|
|||
| 69 | Gemini API «под-18» обязательство на конечный продукт (независимо от лейблов) | владелец | когда-нибудь | Ф3 / отдельное решение | D39.32, D39.34(7) |
|
||||
| 70 | Action-security gate перед выдачей tools/webfetch (D25 п.5) | бэкенд | когда-нибудь | Ф3 | D39.34(7) |
|
||||
| 71 | Планы research/22: epub-tag-rewrite · Q7-леджер ; + F3-brief из D29.3 (chat-edit · Not-useful-петля · cost-of-fix) + Ф3-скоуп 02-mvp-plan (TMX/TBX · Bertalign · дистилляция 7–14B) | бэкенд | когда-нибудь | Ф3 | D39.34(7) |
|
||||
## Оркестратор №9 + бэкенд — КАЧЕСТВО БАНКА ПРИНЯТО И ЗАЛЕНДЕНО ОБОИМИ ЭТАПАМИ (D39.69), 02.08
|
||||
|
||||
Отчёт: [`archive/reports/BANK_QUALITY_DESIGN_2026-08-01.md`](archive/reports/BANK_QUALITY_DESIGN_2026-08-01.md) (ревью-шапка обоих этапов). Процесс: этап 1 ратифицирован владельцем напрямую; этап 2 до чекеров (не стартовали — коллизий нет); отдельного отчёта стройки нет — компенсировано приёмкой исполнением. Верификация: тесты/парити/голден зелёные; голден 172/172 hex-only независимой маскировкой; «4 серии на живом BANK-FULL» воспроизведено независимо; 4-агентное адверсариальное ревью. Закрыты строки 36а/36в/27/84/73 (+feed_cap из 86); строки 5/18/80 перевязаны на замер/гейт; новые 87 (фикс-лист, несущее «ь» в decl_suffix) и 88 (вопросы Q1–Q8, вкл. ⚠книжные каноны в classifier.md). Отступление стройки от дизайна по оси neuter ПРИНЯТО как улучшение (scoped-фолд по §6.2).
|
||||
|
||||
## Оркестратор №9 — ДВА БЭКЕНД-ПРОМТА ВЫДАНЫ (D39.68): банк-качество (дизайн у бэкендеров) ∥ пакет-чекеров, 01.08
|
||||
|
||||
Хендофф по слову владельца («дизайна от тебя не жду — передай бэкендерам; главное правильные промты»): `BACKEND_BANK_QUALITY_SESSION_PROMPT.md` (этап 1 ресёрч+дизайн $0 → СТОП на ратификацию; этап 2 стройка по релею, после лендинга чекеров) и `BACKEND_CHECKER_PACKAGE_SESSION_PROMPT.md` (стройка: строка 25 целиком + 79; харнесс labels в git; Р2/Р4-контракты; голден-протокол вердикт-двигов). Оба несут НОВЫЕ обязательные блоки: глобальный критерий владельца дословно + МАНДАТ РЕВИЗИИ ПОСЫЛОК (свип собрал долг по «забыли», не по «надо ли» — каждый пункт получает вердикт-тройку посылка/нужность/форма, допустимый исход «не строить»).
|
||||
|
|
@ -154,6 +155,10 @@ Q1 = П1 цель-шов санкционирован («главное бэке
|
|||
|
||||
## Бэкенд
|
||||
|
||||
**Качество банка — этап 1 (дизайн) исполнен (01.08, бэкенд-сессия по `BACKEND_BANK_QUALITY_SESSION_PROMPT`, $0, read-only, НЕ закоммичено).** Отчёт: [archive/reports/BANK_QUALITY_DESIGN_2026-08-01.md](archive/reports/BANK_QUALITY_DESIGN_2026-08-01.md). 6 пунктов свипа сведены в **3 переиспользуемых примитива** (A head-aware кластер-по-морфеме: §1 серия-батчинг + §6 голова 蛊 · B ре-порядок `terminology.Batch`: §1 + feed_cap · C стеммер целевого письма: §3 Decl-шов + omission-бэкстоп чекеров) + дата-правки (тип — ОТДЕЛЬНЫМ классификатором первичным; род neuter enum+seed-lint, ось `RenderFormatVersion`). **Самопроверка — 3 рубежа (грунтинг + анкор-верификация всех 8 tech-debt-якорей CONFIRMED + 5-агентная адверсариальная дизайн-критика): §1 серия-дефиниция «равная длина/1 позиция» и §2 инлайн-тип были BROKEN — переработаны (§1 → head-aware, пере-верифицировано на `BANK-FULL.tsv`: наивное = 19 кластеров вкл. блоб с §2-harm+протагонист, head-aware = 4 годных серии; §2 → классификатор первичным, DO-роль ДО RenderBatch; $0-экран честно пере-скоупнут — провабельно пропускал юаньши/Юаньхай); §5/§6/§7-эмбеддинг WEAKENED вправлены.** §4 (36б второе мнение) и §7-эмбеддинг ЗАКРЫТЫ по нужности; §5 (резюме-слой 80) → пилот-Ф2.5-гейт (проза-суммарайзер закрыт; «дата-состояние свободно от D1» опровергнуто — source-anchored reveal без верификатора); §6-эмиссия головы → полигон-замер + после #10 (seed-половина строится). **First-occurrence-тест ИСПОЛНЕН** ($0, `eval/bank_autonomy/m_firstocc.py`): гипотеза первенства ОПРОВЕРГНУТА (финал==первое 7 == финал==частота 7, большинство — ни то ни другое) ⇒ роль = контекстный ре-решатель, усиливает §1 (со-батч — единственный рычаг). **7 вопросов владельцу** (§9: канон-пин между прогонами · замер 36б · диспозиция резюме · hard-инъективность · вкус авто-транслита реалий юаньци/цзин-ци-шэнь · короткая форма 27 · замер эмиссии головы). **СТОП — ратификация оркестратора/владельца; этап 2 (стройка) — по релею после лендинга пакета-чекеров.** → **РАТИФИЦИРОВАНО ВЛАДЕЛЬЦЕМ, ЭТАП 2 (СТРОЙКА) ИСПОЛНЕН (01.08, та же сессия, $0 — только фейк-провайдер в тестах, НЕ закоммичено, resnapshot НЕ запускался).**
|
||||
|
||||
**Качество банка — этап 2 (стройка) исполнен.** Построены 3 примитива дизайна: **§1 серия-батчинг** (`terminology/series.go` `DetectSeries` head-aware + `Batch` серия-целиком + value-порядок feed_cap; пар-данные `lang.SeriesMorphology` из CJK-письма — инертно для алфавита; **пере-верифицировано на живом `BANK-FULL.tsv`: ровно 4 годных серии, ноль блоба**). **§2 тип-классификатор первичным** (новая фаза перед рендером на общем `runBankRoleBatches` — денежный цикл терминолога зарефакторен в общий, чекпойнт-хеш идентичен; корректный тип роутит conform + праймит RenderBatch + банкуется через `attachClassifiedType` БЕЗ смены эмиссии=без реоткрытия D39.50; своя роль/бюджет/промт `classifier.md`; честный $0 label-экран). **§3 Decl-шов** (`lang.TargetStemmer` примитив C + ru `decl_suffix` данные → `dstFormPresent` принимает косвенные падежи без перечисления форм; **neuter** enum→`injection.txt`/`genderConstraintNote`; **SeedLint** валидирует род громко; ось — **scoped `hasNeuter`-фолд** в `memory_version` по образцу `editor-unverified-section-v1`, НЕ бланкетный `RenderFormatVersion`). **Верификация (5 независимых сигналов):** `go test ./...` зелёно · `-race` чисто на затронутых пакетах · греп-общность: ноль пар/книго-литералов в Go-логике (новая пара — без правки Go) · голден Ш-1 **пере-захвачен, 172/172 hex-only = вердикт-нейтрально** (EmbeddedDataVersion сдвинулся от данных injection/target-ru) · **адверсариальное ревью author≠reviewer (4 измерения × refute-verify): 0 подтверждённых дефектов.** Короткая форма (27) — уже работает через `decl.forms[]`, кода не требует (§9-Q6 владельцу открыт). §4/§5/§6-эмиссия/§7-эмбеддинг — как в дизайне (не строятся/гейтнуты).
|
||||
|
||||
**Общность фаза 2 исполнена (01.08, бэкенд-сессия по `BACKEND_GENERALITY_PHASE2_SESSION_PROMPT`, $0, НЕ закоммичено, resnapshot НЕ запускался).** Отчёт: [archive/reports/GENERALITY_PHASE2_2026-08-01.md](archive/reports/GENERALITY_PHASE2_2026-08-01.md). Построено+верифицировано исполнением: **П0** байты-есть-версия (`lang.EmbeddedVersion()`→снапшот, мут.тест) · **П1** цель-шов слоя 7 (isRuTarget×6→`TargetActive`/`TargetScriptNonLatin` по данным · токенизатор цели с алфавитом из данных + U+0301 = ЕДИНЫЙ шов, репэйнут долг `repair.go:182-189` + закрыт #11 · ExportNormalize-гейт · ruSanitizer→per-run поле Checkers · реестр целей из эмбед-данных) · **П2** скрипт-шов источника (`cjkShare`→`sourceScriptShare` по ОБЪЯВЛЕННОМУ письму + новый `lang-script.txt`/`IsCJKScriptLang` · Hangul в CJK-leak — **живой баг ko/строка 75 ЗАКРЫТ**) · **П3** 話-глава в данные · sourceAbbrevs по языку книги · generic-детект глав (Chapter 12, opt units) · консолидация класса письма (5 сайтов, вторая копия арифметики на шов) · **П4** табличный опц-файл + манифест-по-каналам (обратно-совместим, БЕЗ бампа `packAlgoVersion` — снят самый жёсткий блокер слоя данных) · шестёрка: translitInterjections→данные + #11 · малая пачка: #77 ре-ген-эхо-ручка (дефолт=текущее). **Верификация:** `go test -race ./...` 14/14 · голден Ш-1 **0 verdict/wire / 172 version-only** (вердикт-нейтрально по всему пакету) · майнер-парити EXACT (`n=13618`, катастрофы точны, recall 0.9655) · мут.тесты П0-байт/П1-инертность-en/П2-Hangul PASS · сухой-прогон 2-й пары PASS. Сдвинуто 5 снапшот-осей (`embedded_version` нов + CheapGate/Sanitizer/classifier/chunker бампы) — перекупок НЕТ (D39.63). **10 позиций флагнуто на владельца** (ё-фолд · segmentation-громкая ⚠расходится с рат.тестом `prompt_pack_test.go:193` · EstimateTokens-N-коэфф · encoding-аллоулист · 4 дефекта пака-21 нужен ВНЕ-git размеченный корпус · #78 money/ledger-путь) — §5 отчёта, каждый с причиной+дизайном. **СТОП — приёмка оркестратора.** → **ПРИНЯТО И ЗАЛЕНДЕНО (D39.64, ревью-шапка в отчёте; журнал выше).**
|
||||
|
||||
*(Закрытая хроника пакетов №0–№4 (04–10.07) перенесена в [archive/PROGRESS-2026-07-04-10.md](archive/PROGRESS-2026-07-04-10.md) — ревизия D31.)*
|
||||
|
|
|
|||
|
|
@ -11,17 +11,17 @@
|
|||
|
||||
## Структура
|
||||
|
||||
- `architecture/` — синтез. **Источник истины по решениям — [`05-decisions-log.md`](architecture/05-decisions-log.md) (D1–D39.68); при конфликте с любым доком он выше.**
|
||||
- `architecture/` — синтез. **Источник истины по решениям — [`05-decisions-log.md`](architecture/05-decisions-log.md) (D1–D39.69); при конфликте с любым доком он выше.**
|
||||
- `01-decisions.md` — принципы Р1–Р10; `02-mvp-plan.md` — фазы и приёмка (v3, 09.07); `03-implementation-notes.md` — контракты Фазы 0; `04-unhappy-paths.md` — ~70 режимов отказа → механизм; `06-memory-risk-registry.md` — реестр рисков банка памяти; **[`09-target-architecture.md`](architecture/09-target-architecture.md) — целевая 7-слойная архитектура (D39; статус стройки — шапка-таблица; инвариант общности §0.1)** · [`10-prompt-architecture.md`](architecture/10-prompt-architecture.md) — консолидированная промпт-заметка (концерн 4) · [`12-go-style-notes.md`](architecture/12-go-style-notes.md) — норматив общности §0 + Go-ответы · [`13-tech-debt-anchors.md`](architecture/13-tech-debt-anchors.md) — якоря техдолга (справочник к бэклогу, НЕ трекер). **Исполненные (архив 25.07, санкция владельца, → `archive/architecture/`):** [`07-strategic-review.md`](archive/architecture/07-strategic-review.md) (стратаудит 09.07 — курс исполнен) · [`08-sync-audit-ledger.md`](archive/architecture/08-sync-audit-ledger.md) (ледджер синк-аудита — отработан) · [`11-implementation-plan.md`](archive/architecture/11-implementation-plan.md) (план пака-11 — исполнен целиком); `components.puml`/`pipeline.puml` — диаграммы (перерисованы 25.07 под пост-пак-16 реальность: волновой исполнитель, пакетный сплит пака-15, repair-петля за `enabled:false`, generic content-labels D39.25; владелец смотрит PlantUML-расширением VS Code; вручную НЕ рендерить).
|
||||
- `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), `12-quality-diagnosis`/`13-translator-bakeoff`/`14-quality-empirics`/`14b-meaning-battery` (дуга качества «мерить→строить», закрыты → D30/D32/D37/D38), `15-segmentation-empirics` (exp15: когезия под floor-шумом, фертильность — закрыт → D39.7/D39.8), `16-bank-mining` (exp16: WHICH/WHAT банк-майнинга — закрыт → D39.10).
|
||||
- `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)** · **`21` обзор LLM-транспорта чужих харнессов (23.07: наш транспорт опережает/вровень со всеми 11)** · **`22` доменные харнессы перевода (24.07: калибрующий — ядро подтверждено, впереди COGS/18+/общность/измеренность, позади Q4-выпуск и gate+repair-петля; сиквел `02`/`05`)** — у всех ревью-шапки. ⚠ Часть под superseded-баннерами (01/02/03/04/05/09 и gap-1/2/5) — **читай баннер прежде содержимого**.
|
||||
- `PROGRESS.md` — **журнал** (CURRENT-STATE сверху, ниже хронология; НЕ источник решений).
|
||||
- **Активные хендофф-промты (01.08; всё остальное — в `archive/prompts/` с баннерами-исходами):** [`ORCHESTRATOR_SESSION_PROMPT.md`](ORCHESTRATOR_SESSION_PROMPT.md) (роль/нормы; состояние НЕ дублирует) · [`BACKEND_BANK_QUALITY_SESSION_PROMPT.md`](BACKEND_BANK_QUALITY_SESSION_PROMPT.md) (**ТЕКУЩИЙ бэкендный №1**: качество банка, этап 1 дизайн → СТОП) · [`BACKEND_CHECKER_PACKAGE_SESSION_PROMPT.md`](BACKEND_CHECKER_PACKAGE_SESSION_PROMPT.md) (**ТЕКУЩИЙ бэкендный №2**: пакет-чекеров против labels) · [`POLYGON_PACKAGE4_SESSION_PROMPT.md`](POLYGON_PACKAGE4_SESSION_PROMPT.md) (residual-трекер пилота/18+/echo, отложен; P4-хвост из него закрыт D39.61). Закрытые 30.07–01.08 (промты в `archive/prompts/`, отчёты с ревью-шапками в `archive/reports/`): пак-19 «голос/состояние» (D39.54–56) · холодный мини-прогон (D39.58) · ToS-речек (D39.57) · ресёрч общности фаза 1 (D39.60) · полигон Р6+P4 (D39.61) · **фаза 2 общности (D39.64)** · **полигон «сырьё арки банка»+пробы (D39.65)**.
|
||||
- **Активные хендофф-промты (01.08; всё остальное — в `archive/prompts/` с баннерами-исходами):** [`ORCHESTRATOR_SESSION_PROMPT.md`](ORCHESTRATOR_SESSION_PROMPT.md) (роль/нормы; состояние НЕ дублирует) · [`BACKEND_CHECKER_PACKAGE_SESSION_PROMPT.md`](BACKEND_CHECKER_PACKAGE_SESSION_PROMPT.md) (**ТЕКУЩИЙ бэкендный**: пакет-чекеров против labels; сессия ещё не запущена) · [`POLYGON_PACKAGE4_SESSION_PROMPT.md`](POLYGON_PACKAGE4_SESSION_PROMPT.md) (residual-трекер пилота/18+/echo, отложен; P4-хвост из него закрыт D39.61). Закрытые 30.07–01.08 (промты в `archive/prompts/`, отчёты с ревью-шапками в `archive/reports/`): пак-19 «голос/состояние» (D39.54–56) · холодный мини-прогон (D39.58) · ToS-речек (D39.57) · ресёрч общности фаза 1 (D39.60) · полигон Р6+P4 (D39.61) · **фаза 2 общности (D39.64)** · **полигон «сырьё арки банка»+пробы (D39.65)** · **качество банка, оба этапа (D39.69)**.
|
||||
- `archive/` — `prompts/`: закрытые сессионные промты (только история, инструкции оттуда не исполнять) · `reports/`: отчёты паков/ресёрч-сессий с ревью-шапками (живая фактура приёмок — на них ссылаются D-лог и активные промты) · `architecture/`: исполненные арх-доки (07 стратревью · 08 синк-ледджер · 11 план пака-11) · четыре слайса хроники `PROGRESS-2026-07-{04-10,10-13,13-25,25-31}.md`.
|
||||
|
||||
## Статус (2026-08-01, голова D39.68 — эра «общность → качество банка»)
|
||||
## Статус (2026-08-01, голова D39.69 — эра «общность → качество банка»)
|
||||
|
||||
Полная карта состояния НЕ здесь: фазы/курс/очередь/горизонт — в шапке [`PROGRESS.md`](PROGRESS.md) (CURRENT-STATE + единый бэклог), решения — в [`architecture/05-decisions-log.md`](architecture/05-decisions-log.md) (карта актуальности; свежая голова — с хвоста файла). Коротко: Ф0/Ф1 ✅ · арх-ресет D39 (паки 11–16) ✅ · паки 17–20 ✅ · мини-прогон и холодный прогон приняты (D39.37/58) · карта языковой привязки движка снята (D39.60: книго-ось чиста; ja→ru безопасна без правки Go, en→ru — нет) · Р1–Р4 закрыты (D39.63) · пак-21 растворён (D39.62) · **фаза 2 общности принята (D39.64: цель/скрипт-швы по данным, эмбед-хеш, манифест-по-каналам; голден вердикт-нейтрален)**. Полигон-ветка закрыта D39.65 (строки 21/22, фиксы 36а/36в проверены живьём). Текущее: два бэкенд-промта выданы (D39.68) — банк-качество (дизайн у бэкендеров, этап 1) ∥ пакет-чекеров; оба с мандатом ревизии посылок. Свип полноты исполнен и пост-сверен кодом (D39.66: 951 обязательство, потери возвращены; якоря — architecture/13). ⚠ DeepSeek-0731 сменил веса под слагом — платные прогоны СТОП до ре-пробы (бэклог-строка 74). Дальше: стройка банка ∥ пакет-чекеров → ре-проба flash → ДОБОР ИДЕАЛА (мини-прогон и свип гипотез влиты, D39.67) → вторая пара (ja→ru) → МАСШТАБ → пилот Ф2.5 → Ф3 ридер-IDE.
|
||||
Полная карта состояния НЕ здесь: фазы/курс/очередь/горизонт — в шапке [`PROGRESS.md`](PROGRESS.md) (CURRENT-STATE + единый бэклог), решения — в [`architecture/05-decisions-log.md`](architecture/05-decisions-log.md) (карта актуальности; свежая голова — с хвоста файла). Коротко: Ф0/Ф1 ✅ · арх-ресет D39 (паки 11–16) ✅ · паки 17–20 ✅ · мини-прогон и холодный прогон приняты (D39.37/58) · карта языковой привязки движка снята (D39.60: книго-ось чиста; ja→ru безопасна без правки Go, en→ru — нет) · Р1–Р4 закрыты (D39.63) · пак-21 растворён (D39.62) · **фаза 2 общности принята (D39.64: цель/скрипт-швы по данным, эмбед-хеш, манифест-по-каналам; голден вердикт-нейтрален)**. Полигон-ветка закрыта D39.65 (строки 21/22, фиксы 36а/36в проверены живьём). Банк-пак принят целиком (D39.69: серия-батчинг · тип-классификатор · Decl/neuter). Текущее: пакет-чекеров (промт выдан, не запущен) + фикс-лист 87 + вопросы владельцу 88. Свип полноты исполнен и пост-сверен кодом (D39.66: 951 обязательство, потери возвращены; якоря — architecture/13). ⚠ DeepSeek-0731 сменил веса под слагом — платные прогоны СТОП до ре-пробы (бэклог-строка 74). Дальше: стройка банка ∥ пакет-чекеров → ре-проба flash → ДОБОР ИДЕАЛА (мини-прогон и свип гипотез влиты, D39.67) → вторая пара (ja→ru) → МАСШТАБ → пилот Ф2.5 → Ф3 ридер-IDE.
|
||||
|
||||
## Доступные ключи от моделей
|
||||
DEEPSEEK_API_KEY, ZAI_API_KEY, KIMI_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, XAI_API_KEY, MISTRAL_API_KEY
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,5 +1,7 @@
|
|||
# Промт бэкенд-сессии: КАЧЕСТВО БАНКА — этап 1 ресёрч+дизайн ($0), этап 2 стройка (по ратификации)
|
||||
|
||||
> **⚠ АРХИВ (02.08.2026): исполнен ОБОИМИ этапами и ПРИНЯТ — D39.69.** Этап 1 ратифицирован владельцем напрямую; этап 2 построен той же сессией. Отчёт с ревью-шапкой: [`../reports/BANK_QUALITY_DESIGN_2026-08-01.md`](../reports/BANK_QUALITY_DESIGN_2026-08-01.md). Фикс-лист — бэклог-строка 87; вопросы владельцу Q1–Q8 — строка 88. Инструкции отсюда НЕ исполнять.
|
||||
|
||||
**Выдан 01.08.2026, оркестратор №9; санкция владельца D39.67/68.** Ты — бэкенд-сессия TextMachine. Зона записи: `backend/` + свой отчёт в `docs/archive/reports/`. НЕ коммитить — лендит оркестратор. **Первый деливерабл — эхо-блок ≤10 строк** (первым сообщением ДО работы: как понял скоуп/стоп-точки/$0/зону; продублируй шапкой отчёта; подтверждения не жди — СТОП только в конце этапа).
|
||||
|
||||
⚠ **Денег — НОЛЬ.** Платные вызовы моделей запрещены (развилка 74: DeepSeek-0731 покупает пустое, бьёт и терминолога). Все нужные платные пробы УЖЕ куплены и лежат в отчётах — не докупать. `--resnapshot` не запускать.
|
||||
251
docs/archive/reports/BANK_QUALITY_DESIGN_2026-08-01.md
Normal file
251
docs/archive/reports/BANK_QUALITY_DESIGN_2026-08-01.md
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
# Дизайн-пак КАЧЕСТВА БАНКА — этап 1 (ресёрч+дизайн, $0) + этап 2 (стройка)
|
||||
|
||||
> **✅ ОБА ЭТАПА ПРИНЯТЫ И ЗАЛЕНДЕНЫ (оркестратор №9, 02.08.2026, D39.69).** Процесс: этап 1 ратифицирован ВЛАДЕЛЬЦЕМ напрямую (без релея оркестратора — его право), этап 2 исполнен той же сессией ДО старта пакета-чекеров (коллизий нет — чекер-сессия не запускалась); отдельного файла-отчёта стройки нет (стройка отчитана записью в PROGRESS §Бэкенд) — компенсировано верификацией оркестратора исполнением. Приёмка: `go build`/`vet`/`-race` 14/14 · майнер-парити EXACT · голден-верификация зелёная · **голден сверен независимой маскировкой: 172/172 строки = только 64-hex хеши + 4 токена `embedded_version`, вердикты/wire/stage_text байт-идентичны** · **«ровно 4 серии на живом BANK-FULL» воспроизведено оркестратором независимо** (копия DetectSeries по TSV: 一…九转·甲乙丙丁等·甲/乙/丙等资质·中/初/高阶; 方源/元石/元海 вне кластеров) · 4-агентное адверсариальное ревью.
|
||||
> **Расхождения стройки с дизайном:** 2 объявлены и приняты как УЛУЧШЕНИЯ (ось neuter = scoped `neuter-directive-v1`-фолд в `memory_version` вместо бланкетного `RenderFormatVersion` — лучше по правилу условного фолда §6.2; `head_final` = константа CJK-класса при data-driven классе письма); 4 не объявлены → фикс-лист (строка 87): **(1) ГЛАВНОЕ — в `decl_suffix` нет «ь»: приёмочный кейс дизайна «Фан Юаню»→«Фан Юань» эмпирически ПРОВАЛИВАЕТСЯ, палладиевский класс -нь/-ль не матчится (фикс = одна строка данных + пере-захват голдена), тесты стройки тихо пинят «мечом»/«горы» вместо него;** (2) комментарий `Batch` «request hash independent of ordering» неверен (freq-сорт пермутирует ChunkIdx в RequestHash мульти-батчевых книг; сейчас смягчено сдвигом embedded_version); (3) оверсайз-серия кладётся в ОДИН батч сверх бюджета (дизайн читался как разрезание; ре-допускает переполнение — помнить мину cap-8000 терминолога); (4) `classify_types:true` при выключенном terminology-гейте = тихий no-op + ноль config-тестов classify-веток; (5) мутационного теста neuter-фолда нет (пин только косвенный голденом); (6) стеммер в voice-checker не заведён (дизайн-«учесть» = нулевой исход, не объявлен). **⚠ Отдельно на владельца: `prompts/zh-ru/classifier.md` несёт книжные каноны стенд-книги (方源·青茅山·蛊·元石·灵泉·窍·甲等·家老) — первый пар-промт с книжными термами; по норме «книжный термин в общем пар-слое = утечка» нужна либо санкция, либо жанрово-генерические примеры (вопрос Q8).** Платный порог приёмки §2 «воспроизвести 6/6 harm-фикс» не прогнан ($0-мандат) — добрать при ре-пробе развилки 74.
|
||||
|
||||
> **Бэкенд-сессия, 01.08.2026. По промту `docs/BACKEND_BANK_QUALITY_SESSION_PROMPT.md` (D39.68).** Деливерабл этапа 1. Кода в `backend/` не писал; единственное исключение — санкционированный first-occurrence-тест `eval/bank_autonomy/m_firstocc.py` (§7.7, $0 на артефактах coldrun-a). СТОП в конце — на ратификацию. Стройка (этап 2) — только по отдельному релею, после лендинга пакета-чекеров.
|
||||
>
|
||||
> **⚠ Этот отчёт прошёл 5-агентную адверсариальную дизайн-критику (author≠reviewer, §8) — 2 решения были BROKEN и переработаны (§1 серия-дефиниция → head-aware; §2 инлайн-тип → классификатор ПЕРВИЧНЫМ), 3 WEAKENED вправлены (§5/§6/§7). Текст ниже — уже ПОСЛЕ правок.**
|
||||
|
||||
## ЭХО-БЛОК (≤10 строк)
|
||||
|
||||
1. **Скоуп:** по каждому механизму — вердикт-тройка (посылка/нужность/форма ≥2 альтернатив) + выбранный дизайн (интерфейсы/данные/снапшот-оси/тесты приёмки); пп.1–6 полная форма, п.7 кандидаты 1–2 строки; блок «вопросы владельцу»; итог 3–5 строк в `docs/PROGRESS.md` §Бэкенд.
|
||||
2. **Кода не пишу** (кроме `eval/bank_autonomy/m_firstocc.py`, $0 на артефактах coldrun-a).
|
||||
3. **Деньги — НОЛЬ:** платных вызовов нет, `--resnapshot` не запускаю, пробы не докупаю.
|
||||
4. **Зона:** read-only по `backend/`; пишу только отчёт + разрешённую тест-папку. Пост-чек #10 однознаковых ключей строит параллельная чекер-сессия — НЕ проектирую заново.
|
||||
5. **Стоп-точки:** СТОП в конце этапа 1; этап 2 — по релею оркестратора.
|
||||
6. **Мандат:** ревизия посылок; замер против посылки = СТОП+вопрос владельцу; утверждения о коде грунтованы `file:line`+греп; самопроверка исполнением (грунтинг + анкор-верификация + адверсариальная дизайн-критика — §8).
|
||||
7. **Универсальность:** решения на уровне чистого арх-бэкенда; Go не ветвится по паре/книге; критерий приёмки «пара, которой нет в репо, проходит без правки Go».
|
||||
|
||||
---
|
||||
|
||||
## §0. Глобальный критерий и как читать вердикты
|
||||
|
||||
Мерило (владелец 01.08): **чистый архитектурный бэкенд, который легко писать и поддерживать и который качественно выполняет задачу.** Механизм строится только там, где несёт **качество/деньги** — не ради галочки бэклога. Общность: «заработает ли на паре, которой в репо ещё НЕТ, без правки Go?».
|
||||
|
||||
**Курс (D39.59/D39.67):** премиса «решение о переводе = подпись владельца» (D39.42/47) АМЕНДИРОВАНА — банк обязан приходить с максимально качественными **автономными** переводами, подпись опциональна. Рычаг = автономная верность на **холодном** старте (пустой канон), где несущие факторы §C2-3 мертвы, а консолидатор — терминолог по KWIC (D39.65; §7.7 подтвердил ПОЗИТИВНО: роль ре-решает по контексту, игнорируя порядок/частоту).
|
||||
|
||||
**Сквозная находка (несущий тезис чистоты) — после дизайн-критики:**
|
||||
|
||||
| Примитив | Кому служит | Статус |
|
||||
|---|---|---|
|
||||
| **A. Кластер по общей исходной морфеме, HEAD-AWARE** | §1 серия-батчинг (СТРОИТЬ) · §6 голова 蛊 (ИЗМЕР-ГЕЙТ) · §7 ranked-set (свёрнут в §1) | §1 строится; §6-эмиссия демотирована до полигон-замера (критика §6) |
|
||||
| **B. Ре-упорядочивание кандидатов перед батчингом** | §1 серия-смежность · §7 feed_cap | СТРОИТЬ с §1 (одна правка `terminology.Batch`) |
|
||||
| **C. Лёгкий стеммер целевого письма (accepted-set)** | §3 Decl-шов пост-чека · §7/строка 25 omission-бэкстоп чекеров | СТРОИТЬ, согласованно с чекер-сессией |
|
||||
|
||||
**Строится сейчас:** §1 (head-aware серия + value-порядок B), §2 (классификатор типа ПЕРВИЧНЫМ + честно-скоупнутый $0-экран), §3 (стеммер C + neuter + короткая форма). **Измер-гейт / закрыто:** §4 (36б → полигон-замер), §5 (резюме → пилот Ф2.5), §6 (голова → полигон-замер, seed-половина строится), §7-эмбеддинг (закрыт по нужности). **Все 8 tech-debt-якорей ре-верифицированы адверсариально — CONFIRMED (§8).**
|
||||
|
||||
---
|
||||
|
||||
## §1. Серия-батчинг (36в) — ПОЛНАЯ ФОРМА · переработана дизайн-критикой (была BROKEN)
|
||||
|
||||
### (а) Посылка
|
||||
D39.58/D39.65 (замер 21): финальный холодный банк **серийно несогласован причинно на границе батча** — `甲等`→«класс» (батч 5) против `乙丙丁等`→«ранг» (батч 0). Проба §2 `PROBES` (temp 0): со-батч серии → **согласованное+ДЕТЕРМИНИРОВАННОЕ** родовое слово (等: 3 порознь→1 вместе; экстраполяция `丁等`=«четвёртый разряд»), серий-зависим (`转` no-op). §7.7 (исполнено): роль ре-решает по контексту, игнорируя порядок/частоту ⇒ серийную согласованность **нельзя добыть весами/порядком, только со-батчем**. Посылка **верна.**
|
||||
|
||||
**Причинный корень:** `terminology.Batch` (`terminology.go:919`) режет кандидатов по размеру, сохраняя ЛЕКСИКОГРАФИЧЕСКИЙ порядок ключей (`terminology.go:212`). Серия с общей морфемой-СУФФИКСОМ (`等`) сортируется по РАЗЛИЧАЮЩЕМУСЯ ПРЕФИКСУ (`甲乙丙丁`) → раскол. `Related`=только вложенность (`terminology.go:242`); `甲等`/`乙等` не связаны.
|
||||
|
||||
### (б) Нужность
|
||||
Не делать ⇒ холодный банк серийно-несогласован. Причинность+живой фикс замерены, §7.7 показал: альтернатив (веса/порядок) нет. **Нужно.**
|
||||
|
||||
### (в) Форма — выбрано: **HEAD-AWARE кластер серии + со-батч без раскола (по размеру) + value-порядок (B)**
|
||||
|
||||
> ⚠ **Первичная форма («равная длина, различие в ОДНОЙ позиции») была BROKEN дизайн-критикой и МОЙ собственной верификацией на `BANK-FULL.tsv` (150 поверхностей, $0 read-only):** наивное правило даёт 19 транзитивных кластеров, из них ~4 годных; патология — 11-членный блоб `[侧室,元气,元海,元石,花海,蛊室,蛊师,蛊虫,酒气,酒肆,酒虫]` (содержит ОБА §2-branch-harm юнита `元石/元海`!), `[方家,方正,方源,管家]` (протагонист `方源` freq 189 + управляющий), вырожденный `[点,窍,转]` (одиночные символы). Наивное правило ломает главного героя и УКРЕПЛЯЕТ ровно тот транслит-harm, что чинит §2. **Переопределено ниже.**
|
||||
|
||||
Оперирует над `terminology.Candidate`+`terminology.Batch` (пост-merge; **НЕ трогает майнер-парити** — она на `mr.ranked` до Merge, `miner_parity_test.go:103-118`; concede критика):
|
||||
1. **Отношение «серия» HEAD-AWARE.** Серия = **≥3 поверхностей РАВНОЙ длины, делящих общую ГОЛОВУ (родовое слово) и различающихся ТОЛЬКО в позиции МОДИФИКАТОРА (не головы)**. Сторона головы = **пар-данные (langpack-булев `head_final`)**: для zh/ja голова — трейлинг-руны, различающаяся руна обязана быть НЕ-финальной. Исключить L=1. **Проверено мной на `BANK-FULL.tsv`:** head-aware даёт РОВНО 4 годных серии — `一…九转`, `中/初/高阶`, `甲乙丙丁等`, `甲/乙/丙等资质` — и корректно роняет `元/蛊/酒`-семьи (различие в ГОЛОВЕ `石/海/室/师` ⇒ РАЗНЫЕ сущности), протагониста, вырожденные одиночки. **Реконсиляция §1↔§2:** `元石/元海` различаются в ГОЛОВЕ ⇒ НЕ серия — они принадлежат §2 (тип-harm), не §1. Пример `元石↔元海` из чернового §1 УДАЛЁН.
|
||||
2. **Батчинг.** Сорт `(seriesKey, key)` (серия смежна) + **не резать серию границей батча**. **Кап оверсайза — по РЕНДЕР-РАЗМЕРУ, зеркаля `Batch.maxRunes` (`terminologist.go:59` batchRunes=6000), НЕ по числу членов N** (критика: под-N серия с широким KWIC переполнит бюджет; терминолог уже рассуждает 8063 ток у cap, PROBES §1). Серия крупнее бюджета → свои батчи, не роняется.
|
||||
3. **Value-порядок (примитив B, свёрнут feed_cap):** раз трогаем `Batch`, ОТРЕЗ бюджета — по частоте-desc/типу (наименее частотный хвост, не лексикографический). Закрывает feed_cap (§7) одной правкой.
|
||||
|
||||
**Детерминизм/парити (concede критика):** `terminologyVersion` (лог, не фолд); resume $0; майнер-парити НЕ трогается. Пар-агностичность КОДА держится (руны), но ПРЕЦИЗИЯ скрипт-зависима (общий CJK-символ ≈ морфема; алфавитная минимальная пара care/core — совпадение) ⇒ `head_final`+«серия вкл/выкл» = пар-данные; для не-CJK источника серия-канал по умолчанию OFF данными.
|
||||
|
||||
**Канон-пин родового слова МЕЖДУ прогонами/книгами:**
|
||||
- Внутри книги, автономно: `neighbourAgrees` считает ТОЛЬКО `approved` (`terminologist.go:187`) ⇒ на неподписанном мёртв. Софт-вес авто-соседа — **вопрос владельцу §9-Q1** (тюнинг фактора против D39.65 «веса на холодном не трогать»).
|
||||
- Между книгами: носитель есть (экспорт подписанного глоссария как сид, бэклог 48а). Дизайна в арке не требует.
|
||||
|
||||
### Альтернативы (отклонены)
|
||||
- **«Равная длина, 1 позиция» без head-awareness:** 27/31 (у меня 15/19) ложных кластеров на BANK-FULL, вкл. `方源` и §2-harm — ИЗМЕРЕНО. — **Хардкод `等/转`:** утечка. — **Тюнить веса:** D39.65+§7.7. — **Широкий «≥1 общий Han»:** взрывает семьи. — **Мульти-проход:** D39.65 «не гарантирует», платный.
|
||||
|
||||
### Снапшот-оси
|
||||
`terminologyVersion` (лог, не двигает `memory_version`). Софт-пин (если строить) — тоже туда.
|
||||
|
||||
### Тесты приёмки
|
||||
- Юнит: `{甲乙丙丁等,...}` head-aware ⇒ один батч; `元石/元海` (различие в голове) ⇒ НЕ серия; L=1 ⇒ не кластер; серия > бюджета ⇒ свои батчи (по рендер-размеру), не дропнута.
|
||||
- **Регресс-фикстура из BANK-FULL:** 4 годных серии кластеризуются, 15+ ложных — нет (пин против наивного правила).
|
||||
- Пар-агностичность: `head_final=false`/не-CJK ⇒ канал инертен без правки Go.
|
||||
- Голден Ш-1: `terminologyVersion` двинулась; `content_hash`/wire не-серийных термов — нет.
|
||||
|
||||
---
|
||||
|
||||
## §2. Тип-пере-вывод (36а) — ПОЛНАЯ ФОРМА · переработана дизайн-критикой (была BROKEN)
|
||||
|
||||
### (а) Посылка
|
||||
D39.65: draft-`type` ненадёжен **12–22%**; **2 строгих branch-harm** `元石`→юаньши, `元海`→Юаньхай. Проба §3: **сфокусированный ОТДЕЛЬНЫЙ классификатор той же модели чинит 6/6 harm** (+~10 п.п.). Посылка **верна.**
|
||||
|
||||
**Поток типа (end-to-end):** источник = эвристика (`c.Types[0]`, `miner_emit.go:197`; `formantType` `miner_patterns.go:65`). Гейт эмиссии `name|place|title` (`miner_emit.go:303`). Ветка транслита: тип роутит conform (`terminologist.go:165-170`→`terminology.go:415-420`); миннер сам не транслитерирует (`PalladiusConformance` только скорит, `miner_palladius.go:106`). `RenderBatch` печатает draft-`type` роли как подсказку (`terminology.go:633`). Барьер: `ParseReply` (`terminology.go:855-915`) читает РОВНО `src<TAB>dst`, тип игнорирует (анкор §8: «genuine hard wall — правка И render И parse»).
|
||||
|
||||
### (б) Нужность
|
||||
Не делать ⇒ реалии транслитерируются («юаньши/Юаньхай») — «жалоба владельца». Защита = ХРУПКОЕ контекст-переопределение роли, УЖЕ провалившееся на 元石/元海 (RAWMATERIAL:162). **Нужно.**
|
||||
|
||||
### (в) Форма — выбрано: **ОТДЕЛЬНЫЙ сфокусированный классификатор типа ПЕРВИЧНЫМ (DO-роль-проход ДО `RenderBatch`)**
|
||||
|
||||
> ⚠ **Первичная форма (инлайн-тип + $0-бэкстоп как пояс) была BROKEN дизайн-критикой:** (1) $0-бэкстоп «name/place с многословным строчным dst → демот» ловит НОЛЬ из 2 замеренных harm — `元石→юаньши` ОДНО строчное слово (не многословный), `元海→Юаньхай` ЗАГЛАВНОЕ (мой же тест-приёмки его не трогал); транслит-направление СТРУКТУРНО невидимо $0-экрану без исходного класса. (2) Инлайн эмитит тип ОДНОВРЕМЕННО с dst — поздно, чтобы разбиасить dst ЭТОГО (холодного = несущего) прогона; `RenderBatch` праймит роль неверным `type:name` до dst. (3) Инлайн воссоздаёт ровно диагноз пробы §3: тип ломается КАК «плохо-сделанная побочная колонка при переводе» (PROBES:46) — инлайн кладёт его обратно побочной колонкой. **Инвертировано:**
|
||||
|
||||
- **Классификатор ПЕРВИЧНЫМ.** Отдельный дешёвый проход-классификатор (та же модель, задача СФОКУСИРОВАНА на классе — единственная проверенная 6/6 форма, PROBES §3) над кандидатами **ДО `RenderBatch`**, свой request-hash-осью как у терминолога/repair (`terminologist.go:46`). Корректированный тип: (i) роутит conform-ранжирование ЭТОГО холодного прогона (не после факта), (ii) праймит `RenderBatch` верным типом (роль видит `type:term` → смещена переводить), (iii) роутит эмиссию-режим. Это ЕДИНСТВЕННАЯ форма с путём предотвратить холодный-run harm. `title` в промте-классификаторе ОБЯЗАН явно включать work-title (стихи/песни; майнер их типирует `term`, нет work-title-канала `miner_patterns.go:194`) — пар-данные.
|
||||
- **$0-детерминированный экран — ЧЕСТНО ПЕРЕ-СКОУПНУТ.** НЕ «harm-пояс» (он провабельно пропускает `元石→юаньши` строчное и `元海→Юаньхай` заглавное). Его реальная роль — тёплого-прогона **чистка тип-метки**: флаг `name/place`, чей dst был ПЕРЕВЕДЁН (многословный) — расхождение метки для ревью, НЕ safety-net при отказе роли. Записано прямо.
|
||||
|
||||
**COGS:** классификатор = отдельный дешёвый проход по банку (батчи, как терминолог) — цена реальна, но это ПРОВЕРЕННАЯ форма. Инлайн-тип (дешевле) остаётся **кандидатом на COGS-оптимизацию ТОЛЬКО за живым A/B, воспроизводящим 6/6 harm-фикс** до коммита — не принимать на аргументе цены с нефункциональным поясом.
|
||||
|
||||
**Честный лимит (§7.7): §2 НЕ закрывает всю «слишком китаизированно».** Роль АВТОНОМНО сочиняет пиньинь-транслит для КОРРЕКТНО-типированных `term`-реалий (юаньци/чжэньюань/цзин-ци-шэнь) — вне тип-роутинга. §2 чинит 2 строгих name/place→транслит; авто-транслит реалий — вкус/промпт (§9-Q5).
|
||||
|
||||
### Альтернативы (отклонены)
|
||||
- **Инлайн-тип + $0-бэкстоп:** BROKEN (выше) — оставлен ТОЛЬКО как COGS-кандидат за A/B. — **Только $0-экран:** ловит 0/2 harm. — **Верить draft-типу:** 12–22%.
|
||||
|
||||
### Снапшот-оси
|
||||
Классификатор — дешёвый проход, своя request-hash-ось (`terminologyVersion`/роль-марка). Тип на строке фолдится в `memory_version` (`memory.go:415`) ⇒ `--resnapshot` (repin) при смене типа строки.
|
||||
|
||||
### Тесты приёмки
|
||||
- Классификатор: harm-набор (元石/元海/蛊室/池塘/灵泉/酒肆) → `term` (воспроизвести 6/6 PROBES §3 при стройке — приёмочный порог).
|
||||
- Тип праймит `RenderBatch` ДО dst (юнит: корректированный тип в wire-блоке); conform роутится по корректированному типу.
|
||||
- $0-экран: явный тест, что он ПРОПУСКАЕТ `元石→юаньши`/`元海→Юаньхай` (честность роли — не safety-net); ловит только переведённый многословный.
|
||||
- Пар-агностичность: `Conformance=nil` не падает; work-title в промте (данные).
|
||||
|
||||
---
|
||||
|
||||
## §3. Decl-шов (27+73) + род «гу» (84) — ПОЛНАЯ ФОРМА
|
||||
|
||||
### (а) Посылка
|
||||
D39.65: **Decl 142/142 null**. D39.60: «майненая сторона не пишет Decl ⇒ 149 авто-строк не пройдут пост-чек». Строка 27 (D39.44 Q4): «сокращённая форма = ПОЛЕ записи; пост-чек обязан принимать». Строка 84 (D39.21): канон neuter без носителя, `gender:neuter` — тихий no-op. Грунтовано: `dstFormPresent` (`mempostcheck.go:144-154`) = базовый dst + `declForms`; пустой decl ⇒ ложные флаги (research/14 18–36%). `genderConstraintNote` (`memory.go:815-825`) без neuter; `SeedLint` (`memseed.go:486`) не валидирует род/тип/decl.
|
||||
|
||||
### (б) Нужность
|
||||
- **Decl:** банк-КОНТЕНТ не портится — decl нужен ПОСТ-ЧЕКУ (flagger, не hard-gate `mempostcheck.go:17-28`; hard — бэклог 12) ⇒ цена дыры = ШУМ ложных miss. Ургентность СРЕДНЯЯ. **Нужно, не эмиссией форм ролью.**
|
||||
- **Сокращённая форма (27):** ложный miss на краткой форме. **Нужно** (мелко).
|
||||
- **Род neuter (84):** без носителя канон неисполним → C3-спойлер-рода (строка 82). **Нужно.**
|
||||
|
||||
### (в) Форма
|
||||
**Decl — лёгкий стеммер целевого письма в accepted-set (примитив C), НЕ эмиссия ролью.** `dstFormPresent` + форма от консервативного суффикс-стеммера цели (пар-данные: реестр окончаний ru). ТОТ ЖЕ стеммер = omission-бэкстоп чекеров (строка 25 R4) — строить согласованно. Кормит и voice-checker (`memvoice.go:321`) — учесть. Оговорка precision/recall (`mempostcheck.go:130-143`): монотонно (precision↑/recall↓); КОНСЕРВАТИВНЫЙ + замер на `labels/` до hard-gate (связка бэклог-12).
|
||||
|
||||
**Сокращённая форма (27):** дешевле всего — в существующий `decl.forms[]` (0 схемы/миграции, `memory_version` уже фолдит `r.Decl` `memory.go:417`), через тот же `dstFormPresent`. **§9-Q6:** «сокращённая» = морфоклип (forms[] хватает) или прозвище/диминутив (тогда отдельное поле / `nickname_translation`)?
|
||||
|
||||
**Род neuter (84):** `genderConstraintNote`+ветка `neuter`→`tx.GenderNeuter` (новый `gender_neuter` в `injection.txt`, эмбед per-target — пар-данные); `SeedLint` отбраковывает неизвестные `gender`. **Несущая ось = `RenderFormatVersion` (`memory.go:711`), НЕ `memory_version`** (значение `neuter` уже фолдится `memory.go:416` — оно не двигается при начале РЕНДЕРА; новые wire-байты ловит `RenderFormatVersion`) ⇒ `--resnapshot` едет ТАМ.
|
||||
|
||||
### Альтернативы (отклонены)
|
||||
- Терминолог эмитит decl-формы (целевая морфология, дорого/не работа роли). — Полный pymorphy (тяжёл; для рода-гейта строка 82, для accepted-set избыточен). — Отложить Decl (стеммер дёшев+переиспользуется).
|
||||
|
||||
### Снапшот-оси
|
||||
Стеммер в `dstFormPresent` — в хеш ТОЛЬКО при hard-gate; как flagger — вне. neuter → `RenderFormatVersion`. Краткая форма в `forms[]` → `memory_version`.
|
||||
|
||||
### Тесты приёмки
|
||||
- Стеммер: «Фан Юаню»→«Фан Юань» без decl; консервативность на `labels/`. neuter: `gender:neuter`→текст; `gender:xyz`→`SeedLint` ГРОМКО падает; **компат §9-Q6: отбраковывать ли уже-сохранённые neuter-сиды**.
|
||||
|
||||
---
|
||||
|
||||
## §4. 36б «второе мнение» — ЧЕСТНОЕ ОСПАРИВАНИЕ (закрытие HOLDS по критике)
|
||||
|
||||
### (а) Посылка
|
||||
Строка 5/36б: рецензент другой моделью, несогласия колонкой подписной таблицы.
|
||||
|
||||
### (б) Нужность — **не строить в арке; закрыть с маршрутом в полигон-замер.** (Критик подтвердил закрытие.)
|
||||
- Как ранжировщик ОТКЛОНЁН фактом (D39.46: разрыв до декоя 1.27–1.42 против 0.20 — катастроф-экран, не ранжировщик).
|
||||
- §2 закрывает главный тип-harm. Резидуал: систематика ОДНОГО СЕМЕЙСТВА (draft+терминолог — оба DeepSeek) + сочинённое (§7.7: 8/67 сочинено вне drafts). НО «эффективный ≠ верный» — юаньци/цзин-ци-шэнь возможно-ВЕРНЫ (стандартные транслиты), т.е. рецензент чинил бы возможно-НЕ-дефект.
|
||||
- Ценность рецензента ДРУГОГО семейства над §2+со-батч не измерена; форма = полигон-эксперимент.
|
||||
|
||||
### (в) Форма
|
||||
**Закрыть бэкенд-стройку. Маршрут §9-Q2:** полигон-замер нужности рецензента-другого-семейства ДО стройки; переживёт → тонкая колонка наблюдаемости (не гейт). Реальная диспозиция (Q2 роутит КОНКРЕТНЫЙ замер), не пунт.
|
||||
|
||||
---
|
||||
|
||||
## §5. Резюме-слой памяти (строка 80) — РЕКОМЕНДАЦИЯ · вправлена критикой (WEAKENED)
|
||||
|
||||
### (а) Посылка
|
||||
Строка 80 (D25 п.8–9, D30.8, 06-реестр D1): резюме глава→арка→книга + fact-gate. Грунтовано: механизма НЕТ (`memoryVersion` = approved-глоссарий+voice+address; `snapshot.go:391` «summary» аспирационно); D30.8 (`лог:373`) отложил в Ф2.
|
||||
|
||||
### (б)+(в) Рекомендация: **ЗАКРЫТЬ ФОРМУ ПЛАТНОГО ПРОЗА-СУММАРАЙЗЕРА; строку 80 держать ГЕЙТОМ ПИЛОТА Ф2.5.** (Ядро-действие критик подтвердил на 4 основаниях: D1-компаундинг без пост-чека 06-реестр:60 · неизмеренная нужность на zh/ja→ru 06:120 · арх-коллизия mid-run-append `memory.go:157-159`/D30.8 · неподписанная «опция Б» COGS D30.4.)
|
||||
|
||||
> ⚠ **Две пере-натяжки вправлены критиком:**
|
||||
> 1. **«Детерминированное дата-состояние свободно от D1» — БЫЛО ЛОЖНО, СНЯТО.** Любое АВТОНОМНОЕ заполнение relationship/status-строк = LLM-экстракция (как сегодня dst); а арк/reveal-факты — source-anchored reveal БЕЗ фаерящего ключа (`mempostcheck.go:199-206` объявляет класс вне скоупа by construction). ⇒ дата-строка **РЕ-ИМПОРТИРУЕТ D1-компаундинг в структурной одежде, ЕСЛИ пилот не спроектирует source-anchored ВЕРИФИКАТОР** (целевой индекс пост-reveal-рендеров). «Детерминированное» = ФОРМАТ-фолдимо, НЕ популяция-верифицировано.
|
||||
> 2. **«Резюме ортогонально» → «ратифицированная „вторая половина банка" (D39.66 стр.80, 06-реестр §D, D30.8), но НЕИЗМЕРЕННАЯ на этом стеке — отложена в пилот, НЕ ортогональна».**
|
||||
|
||||
**Первый деливерабл пилота Ф2.5:** «допускает ли автономная нарратив-состояние-строка детерминированный верификатор, или это D1 со схемой?» — если верификатора нет, дата-строка НЕ безопаснее прозы, которую заменяет. Провабельно НЕ покрытые KWIC+окнами классы (что резюме БЫ несло): source-anchored reveal (`mempostcheck.go:199`) + арк-колбэки без ключа (sticky сбрасывается на границе главы `wave.go:85`) — реальны для длинного нарратива, но НЕ измеренный дефект текущей арки. Решение — владельца (§9-Q3).
|
||||
|
||||
---
|
||||
|
||||
## §6. Дыра эмиссии 蛊 (строка 18) — ПОЛНАЯ ФОРМА · вправлена критикой (WEAKENED)
|
||||
|
||||
### (а) Посылка
|
||||
D39.58/строка 18: голова `蛊` не приходит НИ ОДНИМ каналом автономно. Грунтовано (шире, чем черновик): `emissionEligible` (`miner_emit.go:302-313`) бьёт (i) тип-гейтом (`蛊` бес-типовый V-A) (ii) длиной (runeLen<2). **Композиты `蛊师/月光蛊` ТОЖЕ вне эмитируемой дельты** — `formantType(蛊)="term"` (`miner_patterns.go:65`), тип-гейт `term` не пускает; живут в кандидатном СЕТЕ (`mr.ranked`; парити `蛊`@1/`蛊师`@2 `miner_parity_test.go:113`), до роли — через БАНКНОТУ. Доменная морфема `蛊` УЖЕ детектируется `detectFormants`/`charOverRep` (`miner_patterns.go:88-148`, `miner.go:46` `FormantMinPartners:3`/`FormantMinOverRep:15`) БЕЗ литерала ⇒ обобщается. «freq 62» — цифра COLDRUN, не грунтована кодом.
|
||||
|
||||
### (б) Нужность — расщепить
|
||||
- Центральный термин книги (в заглавии/брифе) — сид-концерн; майнер для ХВОСТА. Автономная нужность одиночной головы — СРЕДНЯЯ (концессия).
|
||||
- Курс D39.67 = не полагаться на ручной труд.
|
||||
|
||||
### (в) Форма — вправлено критиком: **seed-половину СТРОИТЬ; эмиссию-канал ДЕМОТИРОВАТЬ до полигон-замера + после #10**
|
||||
|
||||
> ⚠ **Критик (WEAKENED):** единственное качество-обоснование канала — «пин головы → согласованные композиты» — НЕ доставлено построенным механизмом и НЕИЗМЕРЕНО (каждый брат-механизм §1/§2 заработал стройку живой пробой; §6 — нет). Ре-скоуп:
|
||||
- **СТРОИТЬ сейчас — только честный дефолт-сид** (uncontested): определяющий термин книги сидится (`allow_short:true`, D39.50, легитимная курация; чек-лист сида). Покрывает единственный терм, что важен.
|
||||
- **ДЕМОТИРОВАТЬ эмиссию-канал до ИЗМЕР-ГЕЙТА:** $0/дешёвый полигон-замер «меняет ли эмиссия голой головы РЕНДЕР композитов (蛊师/月光蛊/本命蛊) против не-эмиссии?». Строить ТОЛЬКО если замер покажет эффект — и **ПОСЛЕ лендинга #10** (одно-символьный пост-чек), НЕ параллельно: иначе `allow_short` одно-символьный ключ фаерит в КАЖДОМ композите (`memory.go:302` `转`-в-`转身`), а #10 это чинит.
|
||||
- **Перед любой стройкой резолвить:** (i) `≥K-композитов`-гейт СЕЙЧАС само-провальный (term-формант-композиты эмиссия-блокированы `miner_emit.go:303` → K=0 для `蛊`) ИЛИ избыточен с `detectFormants.minPartners` (`miner.go:46`) — переформулировать; (ii) KWIC-дизамбигуация одиночной головы (`kwicFor` `terminology.go:285` матчит `蛊` ВНУТРИ каждого композита → грязный контекст-мешок для консолидации).
|
||||
|
||||
**Диспозиция §9-Q7:** заказать ли полигон-замер эффекта эмиссии головы?
|
||||
|
||||
### Снапшот-оси
|
||||
Эмиссия — майнер-версия (не двигает `memory_version` до подписи). Приёмный `allow_short` фолдится (`memory.go:420`) ⇒ `--resnapshot`. Канал за флагом — парити байт-идентична off.
|
||||
|
||||
### Тесты приёмки (если построится после замера+#10)
|
||||
- Синтетика `{XY,ZX,WX}` (общая одиночная морфема, freq≥порог, ≥K композита) ⇒ эмит; редкая ⇒ нет. Майнер-парити: корпус без общих голов ⇒ байт-идентично.
|
||||
|
||||
---
|
||||
|
||||
## §7. Кандидаты — вердикт-тройка 1–2 строки
|
||||
|
||||
- **36г `{{banknote_format}}` (Р5):** УЖЕ — парсер в ОДНОМ файле (`pipeline/banknote.go:33/74/44/80`), проза в ОДНОМ промте (`translator-banknote.md`; НЕ «каждый»). **НИЗКО** (гигиена, линты держат `config/prompt_lint_test.go`). **Форма:** плейсхолдер `{{banknote_format}}` (закрытый набор `render.go:180` +1) ТОЛЬКО в форме, ГЕНЕРИРУЕМОЙ из констант парсера (иначе перенос дубля; `bankTypeOK` map → сортировать); worked-example в промте. Оппортунистически, не standalone.
|
||||
- **STITCH-ретирование:** research/15 (гасить инъекцию после N чанков). **НИЗКО** — счётчика нет (`priorityRank` `memory.go:647-661`); ретируемые термы ВЫСОКОточны (не мусор A1/A2). **Форма:** закрыть до давления на МАСШТАБЕ; **реопен-триггер = бюджет ИЛИ измеренный эффект размера-инъекции на качество** (A6-lost-in-middle, критик — маргинальное качество-измерение, не только COGS). Дрейф-риск пост-чек-ловится.
|
||||
- **Ranked-set:** инъективность dst warning-only ПО ДИЗАЙНУ (`memory.go:1104`). **КАЧЕСТВО закрывает §1** (упорядоченная серия head-aware). Отдельный тип+hard — низко; hard-режим **§9-Q4**.
|
||||
- **tmctl «замена термина с главы N»:** механизм ПОСТРОЕН (`repin.go`+`rebill.go`, D39.42 п.5; команд `translate|report|status|export|redrive|seed-lint` `main.go:72-87`, замены нет). **РЕАЛЬНА** (операторский поток). **Форма:** тонкая CLI над построенным; пак операторского протокола (строка 49).
|
||||
- **feed_cap:** гейт режет лексикографический хвост (`terminologist.go:261-266`). **МАСШТАБ-гейтед.** **Форма:** свёрнут в §1 (примитив B value-порядок) — строить с §1.
|
||||
- **Эмбеддинг A7/D2:** **закрыть по НУЖНОСТИ, не по детерминизму** (критик вправил категор-ошибку). Две формы: (1) hot-path ретривер на промахах — ломает `request_hash`-фолд, верно запрещён (Р3, `migrate.go:248`); (2) РЕГИСТРОВАЯ A7/D2 (06-реестр:32/61/105) — BATCH-TIME, ВНЕ hot-path, low-trust генератор кандидатов, выход human/judge-промоутится во frozen-банк — арх-ИДЕНТИЧНА LLM-терминологу, что уже населяет банк, фолдится в `memory_version` (`snapshot.go:179-190`); НЕ детерминизм-блокирована. **Закрыть на ПРЕМИСЕ:** exact-match recall на автономном холодном НЕ измерен недостаточным. **Реопен = измеренный recall-разрыв на МАСШТАБЕ**, лендинг как batch-time лейн (популяция банка), НИКОГДА hot-path.
|
||||
- **First-occurrence-тест:** ИСПОЛНЕН — §7.7.
|
||||
|
||||
### §7.7. First-occurrence-тест (санкционированный $0, ИСПОЛНЕН)
|
||||
Код: `eval/bank_autonomy/m_firstocc.py` (единицы ДО агрегата; ничьи/сочинённые/катастрофы экранированы). Данные (author≠reviewer): первое-вхождение-в-БАНКНОТУ = `retrieval_state.banknote_detail`; частотный-топ+ФИНАЛ = `parse_bankstop`; джойн EXACT; покрытие 67/75.
|
||||
|
||||
**Результат — гипотеза первенства ОПРОВЕРГНУТА как механизм; роль-консолидатор ПОДТВЕРЖДЁН ПОЗИТИВНО.** Разделяющая подвыборка (первое ≠ частота, N=23 не-сочинённых): финал==первое **7**, финал==частота **7**, финал==иной черновик **9**, +6 сочинено. ⇒ роль НЕ первовхожденческий и НЕ частотный выбиратель — переопределяет ОБА контекстом (позитивно подтверждает вывод-по-исключению D39.65 §2). **Следствие §1:** серийную согласованность нельзя добыть порядком/весами — только со-батчем. **Следствие §2:** роль АВТОНОМНО сочиняет пиньинь-транслит для корректно-типированных `term`-реалий (§9-Q5).
|
||||
|
||||
**Лимиты:** первое = в БАНКНОТЕ, не в исходнике; N мал, одна книга/модель/холодный; «эффективный ≠ верный»; 8/75 недостижимы (superseded, полный фолд дороже но $0). Не манифактурю сходимость — тест ОПРОВЕРГ гипотезу.
|
||||
|
||||
---
|
||||
|
||||
## §8. Самопроверка (author≠reviewer) — ТРИ рубежа
|
||||
|
||||
1. **Грунтинг (6 агентов):** утверждения о коде грунтованы `file:line`, перепроверены грепом/чтением исходника. Вправлены 3 моих черновых пере-натяжки (композиты `蛊师/月光蛊` вне дельты §6; банкнота-проза в ОДНОМ промте §7; neuter-ось `RenderFormatVersion` §3).
|
||||
2. **Анкор-верификация (отдельный агент):** ВСЕ 8 tech-debt-якорей CONFIRMED против живого кода, 0 REFUTED, номера точны.
|
||||
3. **Адверсариальная ДИЗАЙН-критика (5 агентов, дефолт «решение неверно»):** **2 BROKEN + 3 WEAKENED — ВСЕ вправлены:**
|
||||
- **§1 серия-дефиниция BROKEN** → head-aware (я ПЕРЕ-ВЕРИФИЦИРОВАЛ на `BANK-FULL.tsv` сам: наивное правило = 19 кластеров вкл. блоб с §2-harm + протагонист; head-aware = 4 годных серии). Реконсиляция §1↔§2.
|
||||
- **§2 инлайн-тип BROKEN** → классификатор ПЕРВИЧНЫМ (проверенная 6/6 форма, DO-роль ДО RenderBatch); $0-экран честно пере-скоупнут (не harm-пояс — провабельно пропускает юаньши/Юаньхай).
|
||||
- **§5 WEAKENED** → «дата-состояние свободно от D1» снято (source-anchored reveal без верификатора); «ортогонально» → «ратифицированная вторая половина, неизмеренная».
|
||||
- **§6 WEAKENED** → seed-половина строится, эмиссия-канал → измер-гейт + после #10.
|
||||
- **§7 эмбеддинг WEAKENED** → закрыт по нужности (не детерминизму — категор-ошибка); STITCH-реопен += качество-триггер.
|
||||
4. **First-occurrence-тест ИСПОЛНЕН** ($0). Пост-хок верификация оркестратора при лендинге — второй рубеж, не замена.
|
||||
|
||||
---
|
||||
|
||||
## §9. ВОПРОСЫ ВЛАДЕЛЬЦУ (только реально его — вкус/деньги/продукт)
|
||||
|
||||
- **Q1 (§1, деньги/риск):** строить ли автономный канон-пин родового слова МЕЖДУ прогонами (софт-вес авто-соседа)? Напряжение с D39.65 «веса на холодном не трогать». Реком: НЕ сейчас; вернуться на МАСШТАБЕ.
|
||||
- **Q2 (§4, деньги/продукт):** заказать ли полигон-замер нужности рецензента-другого-семейства (36б)? Реком: да, ДО стройки; бэкенд-стройку 36б закрыть.
|
||||
- **Q3 (§5, продукт/деньги):** подтвердить **закрыть проза-суммарайзер; строку 80 гейтом пилота Ф2.5 (первый деливерабл — «допускает ли нарратив-состояние-строка детерминированный верификатор?»)**? Резюме в арку качества банка не входит (0 измеренных дефектов, «опция Б» неподписана D30.4).
|
||||
- **Q4 (§7 ranked-set):** hard-режим инъективности (сегодня warning-only ПО ДИЗАЙНУ)? Реком: нет; порядок закрыт §1.
|
||||
- **Q5 (§2/§7.7, вкус):** желателен ли АВТОНОМНЫЙ пиньинь-транслит-регистр реалий, что роль сочиняет сама (юаньци/чжэньюань/цзин-ци-шэнь — вне §2), или «слишком китаизированно» → промпт-инструкция роли предпочитать смысл? Вкус/промпт, не тип.
|
||||
- **Q6 (§3, продукт):** «сокращённая форма» (27) = морфоклип (`decl.forms[]` хватает, $0) или прозвище/диминутив (отдельное поле)? + компат: отбраковывать ли уже-сохранённые `gender:neuter`-сиды новым seed-lint?
|
||||
- **Q7 (§6, деньги):** заказать ли $0/дешёвый полигон-замер «меняет ли эмиссия голой головы 蛊 рендер её композитов»? От него — строить ли эмиссию-канал (после #10) или закрыть (seed-половина достаточна).
|
||||
|
||||
---
|
||||
|
||||
## §10. Сдвигаемые снапшот-оси (сводно)
|
||||
- **Не двигают `memory_version` (только `terminologyVersion`/майнер-версия, лог):** §1 серия+value-порядок, §2 классификатор+$0-экран, §6 эмиссия головы (если построится), §7 feed_cap-порядок.
|
||||
- **Двигают `--resnapshot` (точечный repin):** §2 смена ТИПА строки (`memory_version`); §3 neuter (`RenderFormatVersion` — НЕ `memory_version`), краткая форма в `decl.forms` (`memory_version`), `allow_short` головы §6 (`memory_version`); §3 стеммер — в хеш ТОЛЬКО при hard-gate.
|
||||
- **Приёмка этапа 2 (по промту):** «пара, которой нет в репо, без правки Go» · голден Ш-1 (пере-захват раз, маскированный дифф, вскрытые единицы, вердикт-двиги ВЕРНЫ) · майнер-парити EXACT · `-race` весь пакет · мутационные тесты на новые швы.
|
||||
|
||||
---
|
||||
|
||||
## §11. Итог (в §Бэкенд PROGRESS.md — 3–5 строк)
|
||||
Дизайн-пак качества банка (этап 1, $0, кода не писал кроме санкц. `m_firstocc.py`): 6 пунктов → **3 переиспользуемых примитива** (A HEAD-AWARE кластер-по-морфеме: §1 серия+§6 голова · B ре-порядок батча: §1+feed_cap · C стеммер-цели: §3 Decl+omission-бэкстоп чекеров) + дата-правки (тип КЛАССИФИКАТОРОМ-ПЕРВИЧНЫМ; род neuter enum+seed-lint, ось `RenderFormatVersion`). **Пройдено 3 рубежа самопроверки (грунтинг+анкор-верификация+5-агентная дизайн-критика): §1 серия-дефиниция и §2 инлайн-тип были BROKEN — переработаны (§1 head-aware, я пере-верифицировал на BANK-FULL сам; §2 классификатор первичным); §5/§6/§7 WEAKENED-правки вправлены.** §4 (36б) и §7-эмбеддинг ЗАКРЫТЫ по нужности; §5 (резюме) и §6-эмиссия — гейтнуты замером. First-occurrence-тест ($0): гипотеза первенства ОПРОВЕРГНУТА, роль-консолидатор подтверждён позитивно. 7 вопросов владельцу (§9). СТОП на ратификацию; этап 2 — по релею после чекеров.
|
||||
|
|
@ -14,6 +14,7 @@
|
|||
- `m21_consistency.py`/`m21b.py` — строка 21: консистентность терминолога по 3 вложенным проходам (n≥2=127, нестаб 43.3%).
|
||||
- `m22_consolidation.py`/`m22b.py` — строка 22: веса §C2-3 (n≥2=75; term-топ==частота 43/43; final≠топ 50/75).
|
||||
- `m36_typeaudit.py`/`m36b_grade.py` — аудит типов 36а (12–22% мис-тип; branch-harm 元石/元海; decl 142/142 null).
|
||||
- `m_firstocc.py` — first-occurrence-тест KWIC-консолидатора (дизайн-пак банка §7.7, 01.08): первое-вхождение-в-БАНКНОТУ (`retrieval_state.banknote_detail`, порядок `(chapter,chunk_idx)`) vs частотный-топ vs ФИНАЛ. Единицы печатаются ДО агрегата (норма «вывод на агрегате до вскрытия единиц»). Итог: гипотеза первенства ОПРОВЕРГНУТА (финал==первое 7 == финал==частота 7, большинство — ни то ни другое) ⇒ роль = контекстный ре-решатель, не выбиратель по порядку/частоте.
|
||||
|
||||
## Платные пробы (term_probe.py, venv eval/.venv)
|
||||
Реконструирует вход терминолога байт-близко к `terminology.go:RenderBatch`, бьёт в DeepSeek напрямую.
|
||||
|
|
|
|||
115
eval/bank_autonomy/m_firstocc.py
Normal file
115
eval/bank_autonomy/m_firstocc.py
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
#!/usr/bin/env python3
|
||||
"""FIRST-OCCURRENCE test of the KWIC consolidator (backlog 73 candidate). $0 — no model calls.
|
||||
|
||||
Positive test of the hypothesis "the terminologist is a KWIC/first-occurrence consolidator, not a
|
||||
frequency-majority picker" (vs the conclusion-BY-EXCLUSION of D39.65 §2). Reads only durable coldrun-a
|
||||
artifacts read-only (D39.58).
|
||||
|
||||
DATA (grounded by author!=reviewer, 01.08):
|
||||
- first-occurrence-in-BANKNOTE per source term: retrieval_state.banknote_detail (JSON {k,s,d,t}),
|
||||
ordered by (chapter, chunk_idx), first-wins. NOTE the honest scope: this is first-seen-in-BANKNOTE
|
||||
(when the drafter first DECLARED the term), NOT first-appearance-in-source-text.
|
||||
- freq-majority top + FINAL terminologist dst: parse_bankstop() (drafts: are score/freq-ordered;
|
||||
r['dst'] is the terminologist FINAL, == terminologist checkpoint output).
|
||||
- join is EXACT on the normalized source key (banknote k is already NormalizeSourceKey'd; bank-stop
|
||||
src is raw Han → for Han these coincide; we also try a light-normalized fallback and REPORT coverage).
|
||||
|
||||
DISCIPLINE (eval-aggregation lesson + prompt): UNITS ARE PRINTED FIRST. The discriminating subpopulation
|
||||
(first-occ != freq-top) is isolated and read unit-by-unit BEFORE any aggregate; ties (first==freq),
|
||||
coined FINALs (== neither draft), and catastrophes are screened against the winner too. No aggregate is
|
||||
emitted before the units.
|
||||
"""
|
||||
import json, sys
|
||||
from parse_common import con, parse_bankstop
|
||||
|
||||
def freq_top(variants): # inlined (do not import m22: its module-level analysis would re-run)
|
||||
return sorted(variants, key=lambda x: (-x[1], x[0]))[0][0]
|
||||
|
||||
def norm(s):
|
||||
return " ".join(s.strip().lower().replace("«", "").replace("»", "").replace('"', "").split())
|
||||
|
||||
# --- first-occurrence-in-banknote per normalized source key ---
|
||||
c = con()
|
||||
firstocc = {} # k -> (chapter, chunk_idx, dst)
|
||||
nchunks = 0
|
||||
for chapter, chunk_idx, detail in c.execute(
|
||||
"SELECT chapter, chunk_idx, banknote_detail FROM retrieval_state "
|
||||
"WHERE banknote_detail != '' ORDER BY chapter, chunk_idx"):
|
||||
nchunks += 1
|
||||
try:
|
||||
arr = json.loads(detail)
|
||||
except Exception:
|
||||
continue
|
||||
for e in arr:
|
||||
k = e.get("k", "")
|
||||
if k and k not in firstocc:
|
||||
firstocc[k] = (chapter, chunk_idx, e.get("d", ""))
|
||||
c.close()
|
||||
print(f"# banknote chunks with detail = {nchunks}; distinct first-occ keys = {len(firstocc)}")
|
||||
|
||||
bs = parse_bankstop()
|
||||
# n>=2 population: terms with >=2 DISTINCT draft variants (the only ones where a consolidation CHOICE exists)
|
||||
n2 = {s: r for s, r in bs.items() if len({d for d, _ in r["drafts"]}) >= 2}
|
||||
|
||||
# join bank-stop term -> its first-occ dst; try raw then light-normalized key
|
||||
def firstocc_for(src):
|
||||
if src in firstocc:
|
||||
return firstocc[src]
|
||||
nk = norm(src)
|
||||
for k, v in firstocc.items():
|
||||
if norm(k) == nk:
|
||||
return v
|
||||
return None
|
||||
|
||||
reached = {s: r for s, r in n2.items() if firstocc_for(s) is not None}
|
||||
print(f"# n>=2 (>=2 distinct drafts) = {len(n2)}; reached via banknote first-occ = {len(reached)}\n")
|
||||
|
||||
# ============ UNITS FIRST — the discriminating subpopulation (first-occ != freq-top) ============
|
||||
rows = []
|
||||
for s, r in reached.items():
|
||||
ch, ci, first_d = firstocc_for(s)
|
||||
drafts = r["drafts"]
|
||||
ft = freq_top(drafts)
|
||||
final = r["dst"]
|
||||
draft_set = {norm(d) for d, _ in drafts}
|
||||
label_first = norm(final) == norm(first_d)
|
||||
label_freq = norm(final) == norm(ft)
|
||||
coined = norm(final) not in draft_set
|
||||
rows.append(dict(src=s, typ=r.get("type", ""), ch=ch, ci=ci, first_d=first_d, ft=ft,
|
||||
final=final, drafts=[d for d, _ in drafts],
|
||||
first_eq_freq=(norm(first_d) == norm(ft)),
|
||||
label_first=label_first, label_freq=label_freq, coined=coined))
|
||||
|
||||
discriminating = [x for x in rows if not x["first_eq_freq"]]
|
||||
print(f"===== DISCRIMINATING SUBPOPULATION: first-occ != freq-top (N={len(discriminating)} of {len(rows)} reached) =====")
|
||||
print("(units read individually BEFORE any aggregate)\n")
|
||||
for x in sorted(discriminating, key=lambda z: (z["ch"], z["ci"])):
|
||||
tag = "FINAL=first" if x["label_first"] and not x["label_freq"] else \
|
||||
"FINAL=freq" if x["label_freq"] and not x["label_first"] else \
|
||||
"FINAL=both" if x["label_first"] and x["label_freq"] else \
|
||||
"FINAL=COINED" if x["coined"] else "FINAL=other-draft"
|
||||
print(f" {x['src']} [{x['typ']}] ch{x['ch']}.{x['ci']} first='{x['first_d']}' freq='{x['ft']}' "
|
||||
f"FINAL='{x['final']}' -> {tag}")
|
||||
print(f" drafts={x['drafts']}")
|
||||
|
||||
# ties (first==freq: NON-discriminating) and coined — screened separately
|
||||
ties = [x for x in rows if x["first_eq_freq"]]
|
||||
coined_all = [x for x in rows if x["coined"]]
|
||||
print(f"\n===== NON-DISCRIMINATING (first-occ == freq-top): N={len(ties)} (cannot separate the two hypotheses) =====")
|
||||
print(f"===== COINED FINALs (== neither any draft variant): N={len(coined_all)} (role invented, screened separately) =====")
|
||||
for x in coined_all[:20]:
|
||||
print(f" {x['src']} [{x['typ']}] FINAL='{x['final']}' drafts={x['drafts']} first='{x['first_d']}'")
|
||||
|
||||
# ============ AGGREGATE (only over the DISCRIMINATING, non-coined units) ============
|
||||
disc_nc = [x for x in discriminating if not x["coined"]]
|
||||
first_only = sum(1 for x in disc_nc if x["label_first"] and not x["label_freq"])
|
||||
freq_only = sum(1 for x in disc_nc if x["label_freq"] and not x["label_first"])
|
||||
other_draft = sum(1 for x in disc_nc if not x["label_first"] and not x["label_freq"])
|
||||
disc_coined = sum(1 for x in discriminating if x["coined"])
|
||||
print(f"\n===== AGGREGATE over DISCRIMINATING non-coined units (N={len(disc_nc)}) =====")
|
||||
print(f" FINAL == first-occ ONLY : {first_only}")
|
||||
print(f" FINAL == freq-top ONLY : {freq_only}")
|
||||
print(f" FINAL == some other draft (neither first nor freq): {other_draft}")
|
||||
print(f" (+ {disc_coined} discriminating units where FINAL was COINED — excluded from the ratio)")
|
||||
print("\nNOTE (honest scope): 'first-occ' = first-seen-in-BANKNOTE, not first-in-source-text; 8/75 multi-")
|
||||
print("variant terms unreachable via retrieval_state (superseded generations) are OUT of this first cut.")
|
||||
Loading…
Add table
Reference in a new issue