Add trim CLI command

This commit is contained in:
2026-05-08 14:53:59 +00:00
parent 1c0e4438ae
commit ac3dcf2557
7 changed files with 996 additions and 1 deletions

View File

@@ -47,6 +47,17 @@ type MergeOptions struct {
CoalesceGap string
}
// TrimOptions captures raw CLI option values before validation.
type TrimOptions struct {
InputFile string
OutputFile string
ReportFile string
Keep string
Remove string
OutputSchema string
AllowEmpty bool
}
// Config is the validated runtime configuration for a merge invocation.
type Config struct {
InputFiles []string
@@ -66,6 +77,17 @@ type Config struct {
FillerMaxDuration float64
}
// TrimConfig is the validated runtime configuration for a trim invocation.
type TrimConfig struct {
InputFile string
OutputFile string
ReportFile string
Mode string
Selector string
OutputSchema string
AllowEmpty bool
}
// NewMergeConfig validates raw merge options and returns normalized config.
func NewMergeConfig(opts MergeOptions) (Config, error) {
cfg := Config{
@@ -168,6 +190,63 @@ func NewMergeConfig(opts MergeOptions) (Config, error) {
return cfg, nil
}
// NewTrimConfig validates raw trim options and returns normalized config.
func NewTrimConfig(opts TrimOptions) (TrimConfig, error) {
inputFile := filepath.Clean(strings.TrimSpace(opts.InputFile))
if strings.TrimSpace(opts.InputFile) == "" {
return TrimConfig{}, errors.New("--input-file is required")
}
if err := requireFile(inputFile, "--input-file"); err != nil {
return TrimConfig{}, err
}
outputFile, err := normalizeOutputPath(opts.OutputFile, "--output-file")
if err != nil {
return TrimConfig{}, err
}
reportFile := ""
if strings.TrimSpace(opts.ReportFile) != "" {
reportFile, err = normalizeOutputPath(opts.ReportFile, "--report-file")
if err != nil {
return TrimConfig{}, err
}
}
keep := strings.TrimSpace(opts.Keep)
remove := strings.TrimSpace(opts.Remove)
if keep == "" && remove == "" {
return TrimConfig{}, errors.New("exactly one of --keep or --remove is required")
}
if keep != "" && remove != "" {
return TrimConfig{}, errors.New("--keep and --remove are mutually exclusive")
}
mode := "keep"
selector := keep
if remove != "" {
mode = "remove"
selector = remove
}
outputSchema := strings.TrimSpace(opts.OutputSchema)
if outputSchema != "" {
if err := validateOutputSchema(outputSchema); err != nil {
return TrimConfig{}, err
}
}
return TrimConfig{
InputFile: inputFile,
OutputFile: outputFile,
ReportFile: reportFile,
Mode: mode,
Selector: selector,
OutputSchema: outputSchema,
AllowEmpty: opts.AllowEmpty,
}, nil
}
func parseModuleList(value string) ([]string, error) {
value = strings.TrimSpace(value)
if value == "" {

View File

@@ -612,6 +612,105 @@ func TestCoalesceGapRejectsInvalidOverride(t *testing.T) {
}
}
func TestNewTrimConfigRequiresInputAndOutput(t *testing.T) {
dir := t.TempDir()
input := writeTempFile(t, dir, "input.json")
output := filepath.Join(dir, "trimmed.json")
_, err := NewTrimConfig(TrimOptions{
OutputFile: output,
Keep: "1",
})
if err == nil || !strings.Contains(err.Error(), "--input-file is required") {
t.Fatalf("expected input-file required error, got %v", err)
}
_, err = NewTrimConfig(TrimOptions{
InputFile: input,
Keep: "1",
})
if err == nil || !strings.Contains(err.Error(), "--output-file is required") {
t.Fatalf("expected output-file required error, got %v", err)
}
}
func TestNewTrimConfigRequiresExactlyOneSelectorFlag(t *testing.T) {
dir := t.TempDir()
input := writeTempFile(t, dir, "input.json")
output := filepath.Join(dir, "trimmed.json")
_, err := NewTrimConfig(TrimOptions{
InputFile: input,
OutputFile: output,
})
if err == nil || !strings.Contains(err.Error(), "exactly one of --keep or --remove is required") {
t.Fatalf("expected missing selector error, got %v", err)
}
_, err = NewTrimConfig(TrimOptions{
InputFile: input,
OutputFile: output,
Keep: "1",
Remove: "2",
})
if err == nil || !strings.Contains(err.Error(), "mutually exclusive") {
t.Fatalf("expected mutually exclusive selector error, got %v", err)
}
}
func TestNewTrimConfigAcceptsOutputSchemaOverride(t *testing.T) {
dir := t.TempDir()
input := writeTempFile(t, dir, "input.json")
output := filepath.Join(dir, "trimmed.json")
reportPath := filepath.Join(dir, "report.json")
cfg, err := NewTrimConfig(TrimOptions{
InputFile: input,
OutputFile: output,
ReportFile: reportPath,
Remove: "3-5",
OutputSchema: OutputSchemaMinimal,
AllowEmpty: true,
})
if err != nil {
t.Fatalf("config failed: %v", err)
}
if cfg.Mode != "remove" {
t.Fatalf("mode = %q, want remove", cfg.Mode)
}
if cfg.Selector != "3-5" {
t.Fatalf("selector = %q, want 3-5", cfg.Selector)
}
if cfg.OutputSchema != OutputSchemaMinimal {
t.Fatalf("output schema = %q, want %q", cfg.OutputSchema, OutputSchemaMinimal)
}
if !cfg.AllowEmpty {
t.Fatal("allow empty should be true")
}
if cfg.ReportFile != reportPath {
t.Fatalf("report file = %q, want %q", cfg.ReportFile, reportPath)
}
}
func TestNewTrimConfigRejectsInvalidOutputSchemaOverride(t *testing.T) {
dir := t.TempDir()
input := writeTempFile(t, dir, "input.json")
output := filepath.Join(dir, "trimmed.json")
_, err := NewTrimConfig(TrimOptions{
InputFile: input,
OutputFile: output,
Keep: "1",
OutputSchema: "compact",
})
if err == nil {
t.Fatal("expected output schema validation error")
}
if !strings.Contains(err.Error(), "--output-schema must be one of") {
t.Fatalf("unexpected error: %v", err)
}
}
func assertPositiveFloatEnvValidation(t *testing.T, envName string) {
t.Helper()