textmachine/backend/cmd/tmctl/invocation.go

67 lines
3.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"flag"
"fmt"
"io"
"textmachine/backend/internal/pipeline"
)
// invocation.go: разбор аргументов CLI, выделенный в чистую функцию (пакет №4 —
// у tmctl было 0 тестов на контрактные инварианты: bad-flag→exit 1, а не 2;
// селектор redrive; порядок валидации). Контракт флагов/текстов ошибок ЗАМОРОЖЕН
// (D12/D15.3) — эта функция только переносит его в тестируемое место.
// invocation is one parsed tmctl command line.
type invocation struct {
cmd string
cfgPath string
resnapshot bool
asJSON bool
asPlaintext bool
sel pipeline.RedriveSelector
}
// parseInvocation parses os.Args[1:] into an invocation. flagOut receives the
// stdlib's flag diagnostics ("flag provided but not defined" + usage) — main
// passes os.Stderr, tests a buffer; the bytes and their destination are part of
// the frozen contract.
//
// Deliberately preserved quirks (менять = менять контракт):
// - the command NAME is not validated here — `tmctl bogus --config x` reaches
// the dispatch switch (after dotenv/ctx setup) and errors there, while
// `tmctl bogus` without --config errors "--config book.yaml is required";
// - ContinueOnError, НЕ ExitOnError: стдлибовский ExitOnError звал бы
// os.Exit(2) на плохом флаге и КОЛЛАПСИРОВАЛ бы с exit-кодом 2
// (completed-with-flags); возврат ошибки ведёт плохой флаг в default-ветку
// main() → exit 1, оставляя 2 эксклюзивным для флагнутых чанков.
func parseInvocation(args []string, flagOut io.Writer) (invocation, error) {
if len(args) < 1 {
return invocation{}, fmt.Errorf("usage: tmctl <translate|report|status|export|redrive> --config book.yaml")
}
cmd, rest := args[0], args[1:]
fs := flag.NewFlagSet(cmd, flag.ContinueOnError)
fs.SetOutput(flagOut)
cfgPath := fs.String("config", "", "path to book.yaml")
resnapshot := fs.Bool("resnapshot", false, "re-pin existing jobs to the current config snapshot (пере-перевод оплаченных чанков — явное согласие)")
asJSON := fs.Bool("json", false, "status: emit the projection as JSON (stable disposition/flag_reason enums) for CI/IDE")
asPlaintext := fs.Bool("plaintext", false, "export: emit the concatenated human text instead of the default stable JSON")
chapter := fs.Int("chapter", -1, "redrive: restrict to this chapter (default: any)")
chunk := fs.Int("chunk", -1, "redrive: restrict to this chunk index within the chapter (default: any)")
reason := fs.String("reason", "", "redrive: restrict to this flag_reason (default: any)")
dryRun := fs.Bool("dry-run", false, "redrive: report what would be re-attacked without touching anything")
if err := fs.Parse(rest); err != nil {
return invocation{}, err
}
if *cfgPath == "" {
return invocation{}, fmt.Errorf("--config book.yaml is required")
}
return invocation{
cmd: cmd, cfgPath: *cfgPath, resnapshot: *resnapshot, asJSON: *asJSON, asPlaintext: *asPlaintext,
sel: pipeline.RedriveSelector{
Chapter: *chapter, ChunkIdx: *chunk, Reason: *reason, DryRun: *dryRun,
},
}, nil
}