Files
yoloyolo/.planning/milestones/v1.1-phases/06-config-package-and-sound-overrides/06-01-PLAN.md
T
2026-03-26 22:06:36 +01:00

16 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
06-config-package-and-sound-overrides 01 tdd 1
config/config.go
config/config_test.go
go.mod
go.sum
true
CFG-01
CFG-02
CFG-04
CFG-05
truths artifacts key_links
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
path provides exports
config/config.go Load function, parse, validate, merge, discover
Load
path provides min_lines
config/config_test.go Table-driven tests for CFG-01 through CFG-05 100
from to via pattern
config/config.go synth/config.go imports synth.FreqConfig, synth.WaveformType, synth.ClassFreqConfigs, synth.WaveformPresetHarmonics synth.FreqConfig|synth.ClassFreqConfigs|synth.WaveformPresetHarmonics
from to via pattern
config/config.go classify/types.go imports classify.TrafficClass, classify.AllClasses classify.TrafficClass|classify.AllClasses
from to via pattern
config/config.go github.com/BurntSushi/toml toml.DecodeFile, md.Undecoded() 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

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.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:

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:

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.<name>] 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

<success_criteria>

  • 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 </success_criteria>
After completion, create `.planning/phases/06-config-package-and-sound-overrides/06-01-SUMMARY.md`