Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
16 KiB
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 |
|
true |
|
|
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.mdFrom 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
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).
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.
<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>