Add normalize command scaffold
This commit is contained in:
39
internal/cli/normalize.go
Normal file
39
internal/cli/normalize.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/seriatim/internal/config"
|
||||||
|
"gitea.maximumdirect.net/eric/seriatim/internal/normalize"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newNormalizeCommand() *cobra.Command {
|
||||||
|
var opts config.NormalizeOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "normalize",
|
||||||
|
Short: "Normalize a transcript artifact into a standard seriatim output shape",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
normalizeOpts := opts
|
||||||
|
if !cmd.Flags().Changed("output-schema") {
|
||||||
|
normalizeOpts.OutputSchema = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := config.NewNormalizeConfig(normalizeOpts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalize.Run(cmd.Context(), cfg)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.StringVar(&opts.InputFile, "input-file", "", "input transcript JSON file")
|
||||||
|
flags.StringVar(&opts.OutputFile, "output-file", "", "output transcript JSON file")
|
||||||
|
flags.StringVar(&opts.ReportFile, "report-file", "", "optional report JSON file")
|
||||||
|
flags.StringVar(&opts.OutputSchema, "output-schema", config.DefaultOutputSchema, "output JSON schema: seriatim-minimal, seriatim-intermediate, or seriatim-full")
|
||||||
|
flags.StringVar(&opts.OutputModules, "output-modules", config.DefaultOutputModules, "comma-separated output modules")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
104
internal/cli/normalize_test.go
Normal file
104
internal/cli/normalize_test.go
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNormalizeCommandIsRecognized(t *testing.T) {
|
||||||
|
cmd := NewRootCommand()
|
||||||
|
cmd.SetArgs([]string{"normalize", "--help"})
|
||||||
|
if err := cmd.Execute(); err != nil {
|
||||||
|
t.Fatalf("normalize command should be recognized: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeMissingInputFileFails(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
output := filepath.Join(dir, "normalized.json")
|
||||||
|
|
||||||
|
err := executeNormalize(
|
||||||
|
"--output-file", output,
|
||||||
|
)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected missing input-file error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "--input-file is required") {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeMissingOutputFileFails(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
input := writeJSONFile(t, dir, "input.json", `{"segments":[]}`)
|
||||||
|
|
||||||
|
err := executeNormalize(
|
||||||
|
"--input-file", input,
|
||||||
|
)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected missing output-file error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "--output-file is required") {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeInvalidOutputSchemaFails(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
input := writeJSONFile(t, dir, "input.json", `{"segments":[]}`)
|
||||||
|
output := filepath.Join(dir, "normalized.json")
|
||||||
|
|
||||||
|
err := executeNormalize(
|
||||||
|
"--input-file", input,
|
||||||
|
"--output-file", output,
|
||||||
|
"--output-schema", "compact",
|
||||||
|
)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected invalid output schema error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "--output-schema must be one of") {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeInvalidOutputModuleFails(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
input := writeJSONFile(t, dir, "input.json", `{"segments":[]}`)
|
||||||
|
output := filepath.Join(dir, "normalized.json")
|
||||||
|
|
||||||
|
err := executeNormalize(
|
||||||
|
"--input-file", input,
|
||||||
|
"--output-file", output,
|
||||||
|
"--output-modules", "yaml",
|
||||||
|
)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected invalid output module error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "unknown output module") {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeValidFlagsReachNotImplementedBoundary(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
input := writeJSONFile(t, dir, "input.json", `{"segments":[]}`)
|
||||||
|
output := filepath.Join(dir, "normalized.json")
|
||||||
|
|
||||||
|
err := executeNormalize(
|
||||||
|
"--input-file", input,
|
||||||
|
"--output-file", output,
|
||||||
|
)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected not implemented error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "not implemented") {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeNormalize(args ...string) error {
|
||||||
|
cmd := NewRootCommand()
|
||||||
|
cmd.SetArgs(append([]string{"normalize"}, args...))
|
||||||
|
return cmd.Execute()
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ func NewRootCommand() *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd.AddCommand(newMergeCommand())
|
cmd.AddCommand(newMergeCommand())
|
||||||
|
cmd.AddCommand(newNormalizeCommand())
|
||||||
cmd.AddCommand(newTrimCommand())
|
cmd.AddCommand(newTrimCommand())
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,15 @@ type TrimOptions struct {
|
|||||||
AllowEmpty bool
|
AllowEmpty bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NormalizeOptions captures raw CLI option values before validation.
|
||||||
|
type NormalizeOptions struct {
|
||||||
|
InputFile string
|
||||||
|
OutputFile string
|
||||||
|
ReportFile string
|
||||||
|
OutputSchema string
|
||||||
|
OutputModules string
|
||||||
|
}
|
||||||
|
|
||||||
// Config is the validated runtime configuration for a merge invocation.
|
// Config is the validated runtime configuration for a merge invocation.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
InputFiles []string
|
InputFiles []string
|
||||||
@@ -88,6 +97,15 @@ type TrimConfig struct {
|
|||||||
AllowEmpty bool
|
AllowEmpty bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NormalizeConfig is the validated runtime configuration for a normalize invocation.
|
||||||
|
type NormalizeConfig struct {
|
||||||
|
InputFile string
|
||||||
|
OutputFile string
|
||||||
|
ReportFile string
|
||||||
|
OutputSchema string
|
||||||
|
OutputModules []string
|
||||||
|
}
|
||||||
|
|
||||||
// NewMergeConfig validates raw merge options and returns normalized config.
|
// NewMergeConfig validates raw merge options and returns normalized config.
|
||||||
func NewMergeConfig(opts MergeOptions) (Config, error) {
|
func NewMergeConfig(opts MergeOptions) (Config, error) {
|
||||||
cfg := Config{
|
cfg := Config{
|
||||||
@@ -247,6 +265,54 @@ func NewTrimConfig(opts TrimOptions) (TrimConfig, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewNormalizeConfig validates raw normalize options and returns normalized config.
|
||||||
|
func NewNormalizeConfig(opts NormalizeOptions) (NormalizeConfig, error) {
|
||||||
|
inputFile := filepath.Clean(strings.TrimSpace(opts.InputFile))
|
||||||
|
if strings.TrimSpace(opts.InputFile) == "" {
|
||||||
|
return NormalizeConfig{}, errors.New("--input-file is required")
|
||||||
|
}
|
||||||
|
if err := requireFile(inputFile, "--input-file"); err != nil {
|
||||||
|
return NormalizeConfig{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
outputFile, err := normalizeOutputPath(opts.OutputFile, "--output-file")
|
||||||
|
if err != nil {
|
||||||
|
return NormalizeConfig{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
reportFile := ""
|
||||||
|
if strings.TrimSpace(opts.ReportFile) != "" {
|
||||||
|
reportFile, err = normalizeOutputPath(opts.ReportFile, "--report-file")
|
||||||
|
if err != nil {
|
||||||
|
return NormalizeConfig{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
outputSchema, err := resolveOutputSchema(opts.OutputSchema)
|
||||||
|
if err != nil {
|
||||||
|
return NormalizeConfig{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
outputModules, err := parseModuleList(opts.OutputModules)
|
||||||
|
if err != nil {
|
||||||
|
return NormalizeConfig{}, fmt.Errorf("--output-modules: %w", err)
|
||||||
|
}
|
||||||
|
if len(outputModules) == 0 {
|
||||||
|
return NormalizeConfig{}, errors.New("--output-modules must include at least one module")
|
||||||
|
}
|
||||||
|
if err := validateNormalizeOutputModules(outputModules); err != nil {
|
||||||
|
return NormalizeConfig{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return NormalizeConfig{
|
||||||
|
InputFile: inputFile,
|
||||||
|
OutputFile: outputFile,
|
||||||
|
ReportFile: reportFile,
|
||||||
|
OutputSchema: outputSchema,
|
||||||
|
OutputModules: outputModules,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func parseModuleList(value string) ([]string, error) {
|
func parseModuleList(value string) ([]string, error) {
|
||||||
value = strings.TrimSpace(value)
|
value = strings.TrimSpace(value)
|
||||||
if value == "" {
|
if value == "" {
|
||||||
@@ -400,3 +466,12 @@ func contains(values []string, target string) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateNormalizeOutputModules(modules []string) error {
|
||||||
|
for _, module := range modules {
|
||||||
|
if module != "json" {
|
||||||
|
return fmt.Errorf("unknown output module %q", module)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -711,6 +711,107 @@ func TestNewTrimConfigRejectsInvalidOutputSchemaOverride(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewNormalizeConfigRequiresInputFile(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
output := filepath.Join(dir, "normalized.json")
|
||||||
|
|
||||||
|
_, err := NewNormalizeConfig(NormalizeOptions{
|
||||||
|
OutputFile: output,
|
||||||
|
OutputModules: DefaultOutputModules,
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected input-file required error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "--input-file is required") {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewNormalizeConfigRequiresOutputFile(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
input := writeTempFile(t, dir, "input.json")
|
||||||
|
|
||||||
|
_, err := NewNormalizeConfig(NormalizeOptions{
|
||||||
|
InputFile: input,
|
||||||
|
OutputModules: DefaultOutputModules,
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected output-file required error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "--output-file is required") {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewNormalizeConfigResolvesOutputSchemaDefaultAndEnv(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
input := writeTempFile(t, dir, "input.json")
|
||||||
|
output := filepath.Join(dir, "normalized.json")
|
||||||
|
|
||||||
|
t.Setenv(OutputSchemaEnv, "")
|
||||||
|
cfg, err := NewNormalizeConfig(NormalizeOptions{
|
||||||
|
InputFile: input,
|
||||||
|
OutputFile: output,
|
||||||
|
OutputModules: DefaultOutputModules,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("config failed: %v", err)
|
||||||
|
}
|
||||||
|
if cfg.OutputSchema != DefaultOutputSchema {
|
||||||
|
t.Fatalf("output schema = %q, want %q", cfg.OutputSchema, DefaultOutputSchema)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Setenv(OutputSchemaEnv, OutputSchemaMinimal)
|
||||||
|
cfg, err = NewNormalizeConfig(NormalizeOptions{
|
||||||
|
InputFile: input,
|
||||||
|
OutputFile: output,
|
||||||
|
OutputModules: DefaultOutputModules,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("config failed: %v", err)
|
||||||
|
}
|
||||||
|
if cfg.OutputSchema != OutputSchemaMinimal {
|
||||||
|
t.Fatalf("output schema = %q, want %q", cfg.OutputSchema, OutputSchemaMinimal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewNormalizeConfigRejectsInvalidOutputSchema(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
input := writeTempFile(t, dir, "input.json")
|
||||||
|
output := filepath.Join(dir, "normalized.json")
|
||||||
|
|
||||||
|
_, err := NewNormalizeConfig(NormalizeOptions{
|
||||||
|
InputFile: input,
|
||||||
|
OutputFile: output,
|
||||||
|
OutputSchema: "compact",
|
||||||
|
OutputModules: DefaultOutputModules,
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected output schema error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "--output-schema must be one of") {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewNormalizeConfigRejectsUnknownOutputModule(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
input := writeTempFile(t, dir, "input.json")
|
||||||
|
output := filepath.Join(dir, "normalized.json")
|
||||||
|
|
||||||
|
_, err := NewNormalizeConfig(NormalizeOptions{
|
||||||
|
InputFile: input,
|
||||||
|
OutputFile: output,
|
||||||
|
OutputModules: "json,yaml",
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected output module error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "unknown output module") {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func assertPositiveFloatEnvValidation(t *testing.T, envName string) {
|
func assertPositiveFloatEnvValidation(t *testing.T, envName string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|||||||
19
internal/normalize/normalize.go
Normal file
19
internal/normalize/normalize.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package normalize
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/seriatim/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Run validates command wiring for normalize and will later execute
|
||||||
|
// artifact-level normalization.
|
||||||
|
func Run(ctx context.Context, cfg config.NormalizeConfig) error {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Implement transcript normalization transformation.
|
||||||
|
return fmt.Errorf("normalize command is not implemented yet")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user