--- phase: 06-config-package-and-sound-overrides plan: 01 type: tdd wave: 1 depends_on: [] files_modified: - config/config.go - config/config_test.go - go.mod - go.sum autonomous: true requirements: - CFG-01 - CFG-02 - CFG-04 - CFG-05 must_haves: truths: - "Load with explicit path to valid TOML returns merged config map with overrides applied" - "Load with no config file found returns default ClassFreqConfigs unchanged" - "Load with unknown TOML key returns error naming the bad key" - "Load with partial override (only frequency set) leaves waveform unchanged" - "Load with partial override (only waveform set) leaves frequency unchanged" - "Load with unknown class name logs warning and does not error" artifacts: - path: "config/config.go" provides: "Load function, parse, validate, merge, discover" exports: ["Load"] - path: "config/config_test.go" provides: "Table-driven tests for CFG-01 through CFG-05" min_lines: 100 key_links: - from: "config/config.go" to: "synth/config.go" via: "imports synth.FreqConfig, synth.WaveformType, synth.ClassFreqConfigs, synth.WaveformPresetHarmonics" pattern: "synth\\.FreqConfig|synth\\.ClassFreqConfigs|synth\\.WaveformPresetHarmonics" - from: "config/config.go" to: "classify/types.go" via: "imports classify.TrafficClass, classify.AllClasses" pattern: "classify\\.TrafficClass|classify\\.AllClasses" - from: "config/config.go" to: "github.com/BurntSushi/toml" via: "toml.DecodeFile, md.Undecoded()" pattern: "toml\\.DecodeFile|Undecoded" --- Create the `config` package with TOML loading, unknown-key validation, partial-merge semantics, and auto-discovery logic. This is the core of Phase 6 — all config behavior except CLI flag wiring. Purpose: Implements CFG-01 (TOML override), CFG-02 (auto-discovery), CFG-04 (partial override), CFG-05 (unknown key error). The package exposes a single `Load(configPath string)` function that returns a ready-to-use `map[classify.TrafficClass]synth.FreqConfig`. Output: `config/config.go`, `config/config_test.go`, updated `go.mod`/`go.sum` @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/06-config-package-and-sound-overrides/06-CONTEXT.md @.planning/phases/06-config-package-and-sound-overrides/06-RESEARCH.md From synth/config.go: ```go type WaveformType int const ( WaveformCustom WaveformType = iota WaveformSine WaveformSquare WaveformSawtooth WaveformTriangle ) type HarmonicDef struct { Ratio int Amplitude float64 } type FreqConfig struct { BaseHz float64 Harmonics []HarmonicDef Pan float64 WaveformType WaveformType } var ClassFreqConfigs = map[classify.TrafficClass]FreqConfig{ /* 14 entries */ } func WaveformPresetHarmonics(wt WaveformType, baseHz float64, sampleRate int) []HarmonicDef const SampleRate = 44100 ``` From classify/types.go: ```go type TrafficClass string const ( ClassICMP TrafficClass = "ICMP" ClassDNS TrafficClass = "DNS" ClassHTTPS TrafficClass = "HTTPS" ClassHTTP TrafficClass = "HTTP" ClassSSH TrafficClass = "SSH" ClassSMTP TrafficClass = "SMTP" ClassNTP TrafficClass = "NTP" ClassDHCP TrafficClass = "DHCP" ClassOtherTCP TrafficClass = "other-TCP" ClassOtherUDP TrafficClass = "other-UDP" ClassUnknown1 TrafficClass = "unknown-1" ClassUnknown2 TrafficClass = "unknown-2" ClassUnknown3 TrafficClass = "unknown-3" ClassUnknown4 TrafficClass = "unknown-4" ) func AllClasses() []TrafficClass ``` Go module path: `github.com/netsynth/netsynth` Config package: TOML load, validate, merge config/config.go, config/config_test.go - Test: Load(explicitPath) with valid TOML `[sounds.ICMP]\nfrequency = 100.0` returns map where ICMP.BaseHz == 100.0 and all other classes unchanged (CFG-01, CFG-04) - Test: Load(explicitPath) with `[sounds.ICMP]\nwaveform = "square"` returns map where ICMP.WaveformType == WaveformSquare and ICMP.BaseHz unchanged (CFG-04) - Test: Load(explicitPath) with `[sounds.ICMP]\nfrequncy = 440` returns error containing "frequncy" (CFG-05) - Test: Load("") in a directory with no netsynth.toml returns default ClassFreqConfigs map with no error (CFG-02) - Test: Load(explicitPath) where file does not exist returns error containing "not found" (CFG-03 prep) - Test: Load(explicitPath) with `[sounds.BOGUS]\nfrequency = 100.0` returns no error but stderr contains "unknown class" (D-09) - Test: Load(explicitPath) with `[sounds.ICMP]\nfrequency = 100.0\nwaveform = "square"` returns ICMP with both overrides applied and harmonics regenerated - Test: Load(explicitPath) with `[sounds.ICMP]\nwaveform = "invalid"` returns error containing "invalid waveform" - Test: All 14 default classes present in result map regardless of override count Create `config/config.go` with: 1. `SoundOverride` struct with pointer fields `Frequency *float64` and `Waveform *string` (toml tags) 2. `rawConfig` struct with `Sounds map[string]SoundOverride` (toml tag "sounds") 3. `Load(configPath string) (map[classify.TrafficClass]synth.FreqConfig, error)` — public entry point 4. `resolvePath(configPath string) (path string, explicit bool, err error)` — handles D-05 discovery order 5. `discoverPath() string` — probes ./netsynth.toml then ~/.config/netsynth/config.toml 6. `parseFile(path string) (rawConfig, error)` — uses toml.DecodeFile + md.Undecoded() for CFG-05 7. `validate(raw rawConfig) error` — validates waveform strings 8. `merge(defaults map[classify.TrafficClass]synth.FreqConfig, overrides map[string]SoundOverride) map[classify.TrafficClass]synth.FreqConfig` — per-field overlay 9. `copyDefaults() map[classify.TrafficClass]synth.FreqConfig` — shallow copy of ClassFreqConfigs 10. `parseWaveform(s string) (synth.WaveformType, error)` — string-to-enum map 11. `validWaveforms` map: "sine"->WaveformSine, "square"->WaveformSquare, "sawtooth"->WaveformSawtooth, "triangle"->WaveformTriangle Add `github.com/BurntSushi/toml@v1.6.0` to go.mod via `go get`. Per D-03: Per-field overlay merge — only non-nil pointer fields override defaults. Per D-07: Unknown keys detected via md.Undecoded(), error names the key. Per D-09: Unknown class names in [sounds.] produce warning to stderr, not error. Per D-05: Discovery order: --config > ./netsynth.toml > ~/.config/netsynth/config.toml. Per D-06: Only one config file loaded, first found wins. Per D-08: Type mismatches produce clear error with field name. Per D-11: All validation happens before returning — fail fast. When frequency is overridden and WaveformType != WaveformCustom, regenerate Harmonics via WaveformPresetHarmonics(cfg.WaveformType, newBaseHz, synth.SampleRate). When waveform is overridden, regenerate Harmonics via WaveformPresetHarmonics(newWt, cfg.BaseHz, synth.SampleRate). Task 1: Config package — TDD red-green-refactor config/config.go, config/config_test.go, go.mod, go.sum synth/config.go (FreqConfig, WaveformType, ClassFreqConfigs, WaveformPresetHarmonics, SampleRate) classify/types.go (TrafficClass, AllClasses, class constants) go.mod (current dependencies) .planning/phases/06-config-package-and-sound-overrides/06-RESEARCH.md (patterns 1-5, pitfalls 1-4) - TestLoadPartialOverrideFrequency: Load TOML `[sounds.ICMP]\nfrequency = 100.0` -> ICMP.BaseHz == 100.0, ICMP.WaveformType == synth.WaveformCustom (unchanged), DNS.BaseHz == 110.0 (unchanged) - TestLoadPartialOverrideWaveform: Load TOML `[sounds.ICMP]\nwaveform = "square"` -> ICMP.WaveformType == synth.WaveformSquare, ICMP.BaseHz == 65.0 (unchanged), len(ICMP.Harmonics) > 0 - TestLoadBothOverrides: Load TOML `[sounds.ICMP]\nfrequency = 100.0\nwaveform = "square"` -> ICMP.BaseHz == 100.0, ICMP.WaveformType == synth.WaveformSquare - TestLoadUnknownKey: Load TOML `[sounds.ICMP]\nfrequncy = 440` -> error != nil, error contains "frequncy" - TestLoadNoConfig: Load("") in temp dir with no netsynth.toml -> err == nil, result has 14 entries, ICMP.BaseHz == 65.0 - TestLoadExplicitMissing: Load("/nonexistent/file.toml") -> error != nil, error contains "not found" - TestLoadUnknownClass: Load TOML `[sounds.BOGUS]\nfrequency = 100.0` -> err == nil, result has 14 entries (BOGUS not present) - TestLoadInvalidWaveform: Load TOML `[sounds.ICMP]\nwaveform = "invalid"` -> error != nil, error contains "invalid waveform" - TestLoadAllDefaultsPresent: Load with any valid override -> len(result) == 14 **RED phase:** Create `config/config_test.go` with all 9 test functions listed in behavior above. Each test: - Creates a temp TOML file with `os.CreateTemp(t.TempDir(), "*.toml")` - Calls `config.Load(tmpFile.Name())` (or `config.Load("")` for no-config test) - Asserts expected outcomes For TestLoadNoConfig: use `t.Chdir(t.TempDir())` (Go 1.24 testing.T.Chdir) to ensure no netsynth.toml exists in working directory. Create a minimal `config/config.go` with just `package config` and a stub `Load` function returning nil, nil so the test file compiles. Run `go test ./config/... -count=1` — all tests must FAIL (red). **GREEN phase:** Implement `config/config.go` fully: 1. Run `go get github.com/BurntSushi/toml@v1.6.0` to add the dependency. 2. Package declaration and imports: ``` package config imports: errors, fmt, io/fs, os, path/filepath, strings github.com/BurntSushi/toml github.com/netsynth/netsynth/classify github.com/netsynth/netsynth/synth ``` 3. Types: - `SoundOverride` struct: `Frequency *float64 \`toml:"frequency"\``, `Waveform *string \`toml:"waveform"\`` - `rawConfig` struct: `Sounds map[string]SoundOverride \`toml:"sounds"\`` 4. `validWaveforms` var: map[string]synth.WaveformType with entries "sine"->WaveformSine, "square"->WaveformSquare, "sawtooth"->WaveformSawtooth, "triangle"->WaveformTriangle 5. `Load(configPath string) (map[classify.TrafficClass]synth.FreqConfig, error)`: - Call resolvePath(configPath) -> path, explicit, err - If err != nil, return nil, err - If path == "", return copyDefaults(), nil (CFG-02: silent default) - Call parseFile(path) -> raw, err - If err != nil AND explicit AND errors.Is(err, fs.ErrNotExist): return nil, fmt.Errorf("config file not found: %s", configPath) - If err != nil (other): return nil, err - Call validate(raw) -> err; if err, return nil, err - Return merge(copyDefaults(), raw.Sounds), nil 6. `resolvePath(configPath string) (string, bool, error)`: - If configPath != "": return configPath, true, nil - path := discoverPath() - return path, false, nil 7. `discoverPath() string`: - Check `os.Stat("netsynth.toml")` — if err == nil, return "netsynth.toml" - dir, err := os.UserConfigDir(); if err != nil, return "" - p := filepath.Join(dir, "netsynth", "config.toml") - Check os.Stat(p) — if err == nil, return p - return "" 8. `parseFile(path string) (rawConfig, error)`: - var raw rawConfig - md, err := toml.DecodeFile(path, &raw) - If err != nil, return raw, err (this handles file-not-found and parse errors including D-08 type mismatches) - undecoded := md.Undecoded() - If len(undecoded) > 0: keyPath := strings.Join(undecoded[0].String() ... ) — actually undecoded is []toml.Key where Key is []string. Use `undecoded[0].String()` which returns dot-joined path. Return raw, fmt.Errorf("config: unknown key %q — check spelling", undecoded[0].String()) - Return raw, nil 9. `validate(raw rawConfig) error`: - For each className, override in raw.Sounds: - If override.Waveform != nil: call parseWaveform(*override.Waveform); if err, return err 10. `parseWaveform(s string) (synth.WaveformType, error)`: - If wt, ok := validWaveforms[s]; ok: return wt, nil - valid := []string{"sine", "square", "sawtooth", "triangle"} - Return 0, fmt.Errorf("config: invalid waveform %q — valid values: %s", s, strings.Join(valid, ", ")) 11. `copyDefaults() map[classify.TrafficClass]synth.FreqConfig`: - result := make(map[...], len(synth.ClassFreqConfigs)) - For k, v := range synth.ClassFreqConfigs: result[k] = v - Return result - Comment: "Shallow copy is safe because merge assigns fresh Harmonics slices from WaveformPresetHarmonics, never mutates the original." 12. `merge(defaults map[classify.TrafficClass]synth.FreqConfig, overrides map[string]SoundOverride) map[classify.TrafficClass]synth.FreqConfig`: - For className, override := range overrides: - class := classify.TrafficClass(className) - cfg, known := defaults[class] - If !known: fmt.Fprintf(os.Stderr, "Warning: config: unknown class %q (ignored)\n", className); continue (D-09) - If override.Frequency != nil: - cfg.BaseHz = *override.Frequency - If cfg.WaveformType != synth.WaveformCustom: cfg.Harmonics = synth.WaveformPresetHarmonics(cfg.WaveformType, cfg.BaseHz, synth.SampleRate) - If override.Waveform != nil: - wt, _ := parseWaveform(*override.Waveform) (already validated) - cfg.WaveformType = wt - cfg.Harmonics = synth.WaveformPresetHarmonics(wt, cfg.BaseHz, synth.SampleRate) - defaults[class] = cfg - Return defaults Run `go test ./config/... -count=1` — all tests must PASS (green). **REFACTOR:** Review for clarity. Run `go vet ./config/...` clean. cd /home/dev/workspace/yoloyolo && go test ./config/... -count=1 -v - config/config.go contains `func Load(configPath string) (map[classify.TrafficClass]synth.FreqConfig, error)` - config/config.go contains `type SoundOverride struct` with `Frequency *float64` and `Waveform *string` - config/config.go contains `type rawConfig struct` with `Sounds map[string]SoundOverride` - config/config.go contains `toml.DecodeFile` call - config/config.go contains `md.Undecoded()` call - config/config.go contains `validWaveforms` map with 4 entries - config/config.go contains `os.UserConfigDir()` call in discoverPath - config/config_test.go contains at least 9 test functions (TestLoad*) - go.mod contains `github.com/BurntSushi/toml` - `go test ./config/... -count=1` exits 0 - `go vet ./config/...` exits 0 config package exists with Load function that handles TOML parsing, unknown-key detection, partial-merge, auto-discovery, and waveform validation. All 9+ tests pass. BurntSushi/toml dependency in go.mod. - `go test ./config/... -count=1 -v` — all tests pass - `go vet ./config/...` — no issues - `go build ./config/...` — compiles cleanly - config.Load("path/to/valid.toml") returns merged map with overrides applied (CFG-01) - config.Load("") with no config file returns defaults silently (CFG-02) - config.Load("") with partial TOML returns map where unset fields retain defaults (CFG-04) - config.Load("path/to/typo.toml") with unknown key returns error naming the key (CFG-05) - All 14 default classes always present in result map After completion, create `.planning/phases/06-config-package-and-sound-overrides/06-01-SUMMARY.md`