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 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 --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") 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, sel: pipeline.RedriveSelector{ Chapter: *chapter, ChunkIdx: *chunk, Reason: *reason, DryRun: *dryRun, }, }, nil }