// Command minerharness прогоняет БОЕВОЙ майнер банка (`internal/miner`) по произвольной книге, // НЕ ТРОГАЯ репозиторий. Прецедент — пакет-6 (`labels/README.md`): отдельный Go-модуль ВНЕ репо с // `module textmachine/backend/<имя>` и `replace textmachine/backend => <копия>/backend`; имя обязано // начинаться с `textmachine/backend/`, иначе Go запретит импорт `internal/`. // // Отличие от пакета-6, сознательное: `replace` смотрит не в рабочее дерево, а в КОПИЮ бэкенда, // извлечённую `git archive ` — параллельно идёт бэкенд-сессия пака-20, её незакоммиченные // правки (в т.ч. `internal/terminology`, `miner_emit.go`) меняли бы результат под руками. Пин к // коммиту делает прогон воспроизводимым и снимает вопрос «чей это был код». // // Вход: JSONL от `eval/pkg7/split_book.py` ({chapter, chunk_idx, source}). Нормализацию делает // сам харнесс тем же вызовом, что и прод (`text.NormalizeSourceKey`, pipeline/mining.go:57). // Выход: TSV эмитированной дельты + сводка. Ноль сетевых вызовов, ноль моделей, $0. package main import ( "bufio" "encoding/json" "flag" "fmt" "os" "sort" "strings" "textmachine/backend/internal/lang" "textmachine/backend/internal/membank" "textmachine/backend/internal/miner" "textmachine/backend/internal/store" "textmachine/backend/internal/text" ) type row struct { Chapter int `json:"chapter"` ChunkIdx int `json:"chunk_idx"` Source string `json:"source"` } func main() { var ( in = flag.String("in", "", "JSONL чанков (chapter, chunk_idx, source)") contrastP = flag.String("contrast", "", "контраст-корпус (jieba dict.txt)") packRoot = flag.String("langpack", "", "корень langpack (configs/langpacks)") srcLang = flag.String("src", "zh", "язык исходника для langpack") tgtLang = flag.String("tgt", "ru", "целевой язык для langpack") seedPath = flag.String("seed", "", "опциональный сид книги (YAML); пусто = майнить с нуля") outTSV = flag.String("out", "", "куда писать TSV дельты (пусто = stdout)") outYAML = flag.String("out-yaml", "", "опционально: карта подписи в форме DeltaYAML") ) flag.Parse() if *in == "" || *contrastP == "" || *packRoot == "" { fmt.Fprintln(os.Stderr, "usage: minerharness -in chunks.jsonl -contrast dict.txt -langpack [-src zh] [-seed seed.yaml] [-out out.tsv]") os.Exit(2) } chunks, chars, chapters, err := readChunks(*in) if err != nil { fatal(err) } cf, err := os.Open(*contrastP) if err != nil { fatal(err) } defer cf.Close() contrast, err := miner.LoadContrast(cf) if err != nil { fatal(err) } pack, err := lang.Load(*packRoot, *srcLang, *tgtLang) if err != nil { fatal(err) } var seed []store.GlossaryEntry if *seedPath != "" { seed, err = membank.LoadGlossarySeed(*seedPath) if err != nil { fatal(err) } } mined := miner.MineBank(chunks, contrast, seed, nil, miner.FrozenConfig(), pack) w := os.Stdout if *outTSV != "" { f, err := os.Create(*outTSV) if err != nil { fatal(err) } defer f.Close() w = f } bw := bufio.NewWriter(w) defer bw.Flush() fmt.Fprintln(bw, "src\ttype\tfreq\tsince_ch\taliases\tevidence") byType := map[string]int{} byLen := map[int]int{} for _, m := range mined { byType[m.Type]++ byLen[len([]rune(m.Src))]++ fmt.Fprintf(bw, "%s\t%s\t%d\t%d\t%s\t%s\n", m.Src, m.Type, m.Freq, m.SinceCh, strings.Join(m.Aliases, "|"), strings.Join(m.Evidence, "|")) } bw.Flush() if *outYAML != "" { y, err := miner.DeltaYAML(mined, nil) if err != nil { fatal(err) } if err := os.WriteFile(*outYAML, []byte(y), 0o644); err != nil { fatal(err) } } fmt.Fprintf(os.Stderr, "chunks=%d chapters=%d chars=%d pack=%s emitted=%d\n", len(chunks), chapters, chars, pack.Version(), len(mined)) fmt.Fprintf(os.Stderr, "by_type: %s\n", kvSorted(byType)) fmt.Fprintf(os.Stderr, "by_srclen: %s\n", kvIntSorted(byLen)) } func readChunks(path string) (out []miner.Chunk, chars, chapters int, err error) { f, err := os.Open(path) if err != nil { return nil, 0, 0, err } defer f.Close() sc := bufio.NewScanner(f) sc.Buffer(make([]byte, 0, 1<<20), 1<<26) seen := map[int]bool{} for sc.Scan() { line := strings.TrimSpace(sc.Text()) if line == "" { continue } var r row if err := json.Unmarshal([]byte(line), &r); err != nil { return nil, 0, 0, err } chars += len([]rune(r.Source)) seen[r.Chapter] = true out = append(out, miner.Chunk{ Chapter: r.Chapter, ChunkIdx: r.ChunkIdx, NSource: text.NormalizeSourceKey(r.Source), }) } return out, chars, len(seen), sc.Err() } func kvSorted(m map[string]int) string { keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } sort.Strings(keys) var b strings.Builder for i, k := range keys { if i > 0 { b.WriteString(" ") } fmt.Fprintf(&b, "%s=%d", k, m[k]) } return b.String() } func kvIntSorted(m map[int]int) string { keys := make([]int, 0, len(m)) for k := range m { keys = append(keys, k) } sort.Ints(keys) var b strings.Builder for i, k := range keys { if i > 0 { b.WriteString(" ") } fmt.Fprintf(&b, "%d=%d", k, m[k]) } return b.String() } func fatal(err error) { fmt.Fprintln(os.Stderr, "minerharness:", err) os.Exit(1) }