91 lines
4.9 KiB
Go
91 lines
4.9 KiB
Go
package pipeline
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
|
|
"textmachine/backend/internal/store"
|
|
)
|
|
|
|
// mining.go: bank-mining stop boundary (WS3 wired live, R1). Between the drafts done and the edit wave, the
|
|
// miner scores WHICH-candidates over the the draft wave source (the detector is offline — it reads the SOURCE, not the
|
|
// drafts; drafts only mark that a chapter was reached) against the general-zh contrast, emits an
|
|
// alias-clustered seed-delta (default B, WHICH-only — the dst is the owner's to attach via the banknote at
|
|
// sign time), and — on a NON-EMPTY delta — writes the owner SIGNATURE MAP and STOPS before the edit wave. The owner
|
|
// reviews it, promotes terms into the mined-delta file (approved + dst), and re-runs: seedGlossary loads
|
|
// those as Source:mined (moving only edit-wave snapshot), the draft wave resumes at $0, the bank-mining stop re-mines (the now-seeded terms are
|
|
// excluded → the delta empties), and the edit wave runs. Mining is OFF (auto-continue) unless a langpack AND a contrast
|
|
// artifact are both configured — so every $0 test / the golden fixture (no contrast) auto-continues.
|
|
|
|
// signatureMapPath is where the bank-mining stop writes the mined-delta YAML for owner sign — beside the project DB, so it
|
|
// travels with the book state (never in git, like the DB). Deterministic (no time/rand).
|
|
func (r *Runner) signatureMapPath() string {
|
|
return r.Book.ProjectDB + ".mined-signature.yaml"
|
|
}
|
|
|
|
// runBankMiningStop executes the bank-mining stop. Returns stopped=true (with the signature map written and
|
|
// r.lastMinedCount set) when the miner proposes a non-empty delta; stopped=false (auto-continue) when mining
|
|
// is unconfigured or the delta is empty. $0 to providers (the detector is deterministic + offline).
|
|
func (r *Runner) runBankMiningStop(ctx context.Context, chunks []Chunk) (stopped bool, err error) {
|
|
if r.pack == nil || r.Pipeline.Mining.ContrastPath == "" {
|
|
return false, nil // mining not configured → auto-continue to the edit wave
|
|
}
|
|
f, err := os.Open(r.Pipeline.Mining.ContrastPath)
|
|
if err != nil {
|
|
return false, fmt.Errorf("pipeline: the bank-mining stop open mining contrast %s: %w", r.Pipeline.Mining.ContrastPath, err)
|
|
}
|
|
defer f.Close()
|
|
contrast, err := LoadContrast(f)
|
|
if err != nil {
|
|
return false, fmt.Errorf("pipeline: the bank-mining stop load mining contrast %s: %w", r.Pipeline.Mining.ContrastPath, err)
|
|
}
|
|
|
|
// The miner works over the memnorm-normalized SOURCE of every chunk (the candidate space matches the
|
|
// glossary/GT space). Deterministic; the annotation/blurb rule is handled inside MineBank's filters.
|
|
minerChunks := make([]MinerChunk, len(chunks))
|
|
for i, ch := range chunks {
|
|
minerChunks[i] = MinerChunk{Chapter: ch.Chapter, ChunkIdx: ch.ChunkIdx, NSource: normalizeSourceKey(ch.Text)}
|
|
}
|
|
seed, err := r.Store.GlossaryForBook(r.Book.BookID)
|
|
if err != nil {
|
|
return false, fmt.Errorf("pipeline: the bank-mining stop read glossary for mining: %w", err)
|
|
}
|
|
mined := MineBank(minerChunks, contrast, seed, frozenMinerConfig(), r.pack)
|
|
r.lastMinedCount = len(mined)
|
|
if len(mined) == 0 {
|
|
r.Log.InfoContext(ctx, "bank-mining: empty delta, auto-continuing to the edit wave", "book", r.Book.BookID)
|
|
return false, nil
|
|
}
|
|
|
|
// Non-empty delta → write the owner signature map (the mined seed-delta YAML) and STOP before the edit wave.
|
|
yamlDelta, err := MinedDeltaYAML(mined)
|
|
if err != nil {
|
|
return false, fmt.Errorf("pipeline: the bank-mining stop marshal mined delta: %w", err)
|
|
}
|
|
if err := os.WriteFile(r.signatureMapPath(), []byte(yamlDelta), 0o644); err != nil {
|
|
return false, fmt.Errorf("pipeline: the bank-mining stop write signature map %s: %w", r.signatureMapPath(), err)
|
|
}
|
|
r.Log.WarnContext(ctx, "bank-mining: new terms await owner signature; run STOPPED before the edit wave (review the signature map, promote terms into the mined-delta file, then resume)",
|
|
"book", r.Book.BookID, "terms", len(mined), "signature_map", r.signatureMapPath())
|
|
return true, nil
|
|
}
|
|
|
|
// loadMinedDelta reads the owner-curated mined-delta YAML (book.MinedDelta) and stamps every entry
|
|
// Source:"mined" — NOT via loadGlossarySeed (which hardcodes Source:"seed", memseed.go, moving the base
|
|
// bank / draft-wave snapshot). This is the mined-write path (plan §1(в), F2): the mined terms land in the ENRICHED
|
|
// bank version but NOT the base, so adding them moves ONLY edit-wave snapshot («переоплата ОДНА»). Reuses
|
|
// loadGlossarySeed's parser/validation, then re-stamps the Source. Empty path → nil (no mined terms).
|
|
func (r *Runner) loadMinedDelta() ([]store.GlossaryEntry, error) {
|
|
if r.Book.MinedDelta == "" {
|
|
return nil, nil
|
|
}
|
|
entries, err := loadGlossarySeed(r.Book.MinedDelta)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("pipeline: load mined-delta %s: %w", r.Book.MinedDelta, err)
|
|
}
|
|
for i := range entries {
|
|
entries[i].Source = "mined" // override the seed loader's Source:seed → mined (base-excluded)
|
|
}
|
|
return entries, nil
|
|
}
|