textmachine/backend/cmd/tmctl/bankapply.go

63 lines
3.4 KiB
Go

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
}