package main import ( "context" "encoding/json" "fmt" "io" "textmachine/backend/internal/pipeline" ) // bankapply.go: `tmctl bank-apply` — the door a user's correction of the memory bank comes in through. // // It is the engine's half of D39.156: the bank is the product's consistency mechanism, and until this // command existed the two files that carry a decision had no writer anywhere, so a platform could only // deliver a user's correction by pretending to be a human with a text editor. The engine is now the sole // writer of its own schema, which is what lets a caller decide about a term without knowing the format // of anything. // // $0 and key-less, like the other read-side commands: it reads the bank, rewrites two YAML files and // calls no provider. The flag that carries provider keys is refused for it at parse time (D20.4). // bankApply runs the verb and prints its report to stdout. // // The report is printed for every outcome that HAS one, and that is the contract rather than a // convenience: all-or-nothing means a refused set leaves the files untouched, so the list of reasons IS // the product of the call. It goes to stdout while the one-line error goes to stderr — the same split // `status --json` uses when it prints a whole report and then exits non-zero. // // ⚠ The DECISIONS class (14) prints a report in EVERY spelling, the two caps included — the class's own // contract, brought to one carrier. What still speaks on stderr alone: the config class before the lock // (10 — the wiring itself is broken, there is no book to report about), a project another process holds // (12), a project whose schema is not this binary's (13 — the read of the bank is what discovers it), a // stop that lands on the post-read cancellation check (5 — the verdict does not exist yet, so there are // no computed facts to print; the later pre-write check DOES print the stopped report), and a plain // read error on the store (1). A stop earlier than the post-read check is not observed at all: neither // the config load nor the lock consults the context, so the call simply proceeds to that check. // Class 12 is the one an automated caller meets most often, because it is the class it retries. Class 15 (write incomplete) // always carries a report — its `written_*` fields are the difference between «nothing landed» and // «half landed», and re-sending the same document converges either way. func bankApply(ctx context.Context, out io.Writer, cfgPath, decisionsPath string, dryRun bool) error { rep, err := pipeline.ApplyBankDecisions(ctx, cfgPath, decisionsPath, dryRun) // A zero report means the call did not get far enough to have one (an unreadable config, a document // that is not a decisions document); those errors speak for themselves on stderr. if rep.Version != "" { if werr := renderBankDecisions(out, rep); werr != nil && err == nil { return werr } } return err } // renderBankDecisions writes the report as indented JSON with a trailing newline — the shape the other // machine surfaces of this CLI use (manifest --json, status --json), so a consumer parses all of them // the same way. func renderBankDecisions(out io.Writer, rep pipeline.BankDecisionsReport) error { body, err := json.MarshalIndent(rep, "", " ") if err != nil { return fmt.Errorf("tmctl: render the bank-decisions report: %w", err) } _, err = fmt.Fprintf(out, "%s\n", body) return err }