chore: archive v1.1 phase directories to milestones
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,323 @@
|
||||
---
|
||||
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"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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`
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<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
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
|
||||
|
||||
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`
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<feature>
|
||||
<name>Config package: TOML load, validate, merge</name>
|
||||
<files>config/config.go, config/config_test.go</files>
|
||||
<behavior>
|
||||
- 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
|
||||
</behavior>
|
||||
<implementation>
|
||||
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).
|
||||
</implementation>
|
||||
</feature>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Config package — TDD red-green-refactor</name>
|
||||
<files>config/config.go, config/config_test.go, go.mod, go.sum</files>
|
||||
<read_first>
|
||||
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)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- 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
|
||||
</behavior>
|
||||
<action>
|
||||
**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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/dev/workspace/yoloyolo && go test ./config/... -count=1 -v</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 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
|
||||
</acceptance_criteria>
|
||||
<done>
|
||||
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.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `go test ./config/... -count=1 -v` — all tests pass
|
||||
- `go vet ./config/...` — no issues
|
||||
- `go build ./config/...` — compiles cleanly
|
||||
</verification>
|
||||
|
||||
<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>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/06-config-package-and-sound-overrides/06-01-SUMMARY.md`
|
||||
</output>
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
---
|
||||
phase: 06-config-package-and-sound-overrides
|
||||
plan: 01
|
||||
subsystem: config
|
||||
tags: [toml, BurntSushi/toml, config-loading, partial-merge, validation]
|
||||
|
||||
# Dependency graph
|
||||
requires:
|
||||
- phase: 05-waveform-types-and-bank-decoupling
|
||||
provides: WaveformType enum, WaveformPresetHarmonics, FreqConfig.WaveformType field
|
||||
- phase: 01-capture-and-classification
|
||||
provides: classify.TrafficClass, classify.AllClasses, 14 class constants
|
||||
|
||||
provides:
|
||||
- config.Load(configPath string) returns map[classify.TrafficClass]synth.FreqConfig
|
||||
- SoundOverride struct with pointer fields for partial-merge semantics
|
||||
- TOML file parsing with unknown-key detection via BurntSushi/toml Undecoded()
|
||||
- Auto-discovery of ./netsynth.toml and ~/.config/netsynth/config.toml
|
||||
- Per-field overlay merge preserving unspecified defaults
|
||||
- Waveform string validation before merge (fail fast)
|
||||
|
||||
affects:
|
||||
- 06-02 (CLI flag wiring: --config flag passes configPath to config.Load)
|
||||
- encode package (RunSynthesis will accept merged config map from config.Load)
|
||||
|
||||
# Tech tracking
|
||||
tech-stack:
|
||||
added: ["github.com/BurntSushi/toml v1.6.0 — TOML parsing with MetaData.Undecoded() for unknown-key detection"]
|
||||
patterns:
|
||||
- "Pointer fields (*float64, *string) in decode struct for partial-override semantics (nil = not set)"
|
||||
- "parseFile → validate → merge pipeline for fail-fast config loading (D-11)"
|
||||
- "Dedicated config package for testable isolation from Cobra/CLI concerns"
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- config/config.go
|
||||
- config/config_test.go
|
||||
modified:
|
||||
- go.mod
|
||||
- go.sum
|
||||
|
||||
key-decisions:
|
||||
- "Used BurntSushi/toml v1.6.0 over pelletier/go-toml v2 — Undecoded() returns structured []Key (not formatted string), easier to extract key name for error messages"
|
||||
- "Shallow copy in copyDefaults() is safe because merge reconstructs Harmonics via WaveformPresetHarmonics rather than mutating the original slice"
|
||||
- "Unknown class names produce stderr warning (not error) per D-09, preparing for Phase 7 user-defined classes"
|
||||
|
||||
patterns-established:
|
||||
- "Config package is independent of cmd/ — no Cobra imports, fully unit-testable"
|
||||
- "Merge functions take defaults map by value and modify in place, returning it"
|
||||
|
||||
requirements-completed: [CFG-01, CFG-02, CFG-04, CFG-05]
|
||||
|
||||
# Metrics
|
||||
duration: 3min
|
||||
completed: 2026-03-26
|
||||
---
|
||||
|
||||
# Phase 6 Plan 01: Config Package Summary
|
||||
|
||||
**TOML-based config loader with pointer-field partial merge, BurntSushi/toml Undecoded() unknown-key detection, and XDG auto-discovery at ./netsynth.toml and ~/.config/netsynth/config.toml**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** ~3 min
|
||||
- **Started:** 2026-03-26T19:55:08Z
|
||||
- **Completed:** 2026-03-26T19:57:45Z
|
||||
- **Tasks:** 1 (TDD: red → green)
|
||||
- **Files modified:** 4 (config/config.go, config/config_test.go, go.mod, go.sum)
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- Created `config` package with single `Load(configPath string)` public API
|
||||
- Implemented pointer-field partial merge: only non-nil fields override defaults (CFG-04)
|
||||
- Added BurntSushi/toml Undecoded() for field-level typo detection (CFG-05)
|
||||
- Auto-discovery of netsynth.toml in working dir and ~/.config/netsynth/config.toml (CFG-02)
|
||||
- Explicit file missing returns clear error; auto-discovery missing is silent (CFG-02/CFG-03)
|
||||
- Harmonics regenerated via WaveformPresetHarmonics when waveform or frequency is overridden
|
||||
- All 9 tests pass; go vet clean
|
||||
|
||||
## Task Commits
|
||||
|
||||
Each task committed atomically via TDD:
|
||||
|
||||
1. **RED - Failing tests** - `b9ec05a` (test): 9 test functions for CFG-01 through CFG-05
|
||||
2. **GREEN - Full implementation** - `1f877e7` (feat): config.Load, merge, validate, discover
|
||||
|
||||
_Note: TDD task has two commits (RED test stub → GREEN implementation)_
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `config/config.go` - Load(), SoundOverride, rawConfig, merge, validate, discoverPath
|
||||
- `config/config_test.go` - 9 test functions covering all CFG requirements
|
||||
- `go.mod` - Added github.com/BurntSushi/toml v1.6.0
|
||||
- `go.sum` - Updated checksum for new dependency
|
||||
|
||||
## Decisions Made
|
||||
|
||||
- **BurntSushi/toml over pelletier/go-toml v2**: Undecoded() returns `[]toml.Key` ([]string slices) — structured, allowing exact key name extraction for error messages. pelletier's DisallowUnknownFields returns a formatted string (harder to extract just the key name).
|
||||
- **Shallow copy in copyDefaults()**: Safe because merge code always replaces `Harmonics` with a freshly generated slice from WaveformPresetHarmonics rather than mutating the original. Documented with comment for future maintainers.
|
||||
- **Unknown class warning (not error)**: Following D-09 to emit `fmt.Fprintf(os.Stderr, "Warning: ...")` for unknown class names. Phase 7 user-defined classes will be valid, so this is by design.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
None - plan executed exactly as written. The worktree needed a rebase onto master to include phase 05 code (WaveformType, WaveformPresetHarmonics) before starting — this was a prerequisite resolution, not a deviation.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
- Worktree was based on remote origin/master (pre-phase-05). Rebased onto local master to get WaveformType and WaveformPresetHarmonics before implementation. No code conflicts.
|
||||
|
||||
## User Setup Required
|
||||
|
||||
None - no external service configuration required. BurntSushi/toml is fetched automatically via `go get`.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
- `config.Load()` is ready for wiring into `cmd/netsynth/main.go` via `--config` flag (Plan 06-02)
|
||||
- `encode.RunSynthesis` signature change (accept `freqCfgs map[classify.TrafficClass]synth.FreqConfig`) is needed in Plan 06-02
|
||||
- All 14 default classes always present in result map — safe to pass directly to `synth.NewBank()`
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
- FOUND: config/config.go
|
||||
- FOUND: config/config_test.go
|
||||
- FOUND: .planning/phases/06-config-package-and-sound-overrides/06-01-SUMMARY.md
|
||||
- FOUND: b9ec05a (RED commit — failing tests)
|
||||
- FOUND: 1f877e7 (GREEN commit — full implementation)
|
||||
|
||||
---
|
||||
*Phase: 06-config-package-and-sound-overrides*
|
||||
*Completed: 2026-03-26*
|
||||
@@ -0,0 +1,262 @@
|
||||
---
|
||||
phase: 06-config-package-and-sound-overrides
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["06-01"]
|
||||
files_modified:
|
||||
- encode/mp3.go
|
||||
- encode/mp3_test.go
|
||||
- cmd/netsynth/main.go
|
||||
autonomous: true
|
||||
requirements:
|
||||
- CFG-03
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "User passes --config /path/to/file.toml and the tool uses that file for sound overrides"
|
||||
- "User passes --config /nonexistent.toml and the tool exits with a clear error before capture"
|
||||
- "User runs without --config and auto-discovery kicks in (or defaults used silently)"
|
||||
- "RunSynthesis uses the merged config map instead of hardcoded ClassFreqConfigs"
|
||||
artifacts:
|
||||
- path: "cmd/netsynth/main.go"
|
||||
provides: "--config flag, config.Load call, passing merged map to RunSynthesis"
|
||||
contains: "configPath"
|
||||
- path: "encode/mp3.go"
|
||||
provides: "RunSynthesis with freqCfgs parameter"
|
||||
contains: "freqCfgs map[classify.TrafficClass]synth.FreqConfig"
|
||||
- path: "encode/mp3_test.go"
|
||||
provides: "Updated tests for new RunSynthesis signature"
|
||||
key_links:
|
||||
- from: "cmd/netsynth/main.go"
|
||||
to: "config/config.go"
|
||||
via: "config.Load(configPath)"
|
||||
pattern: "config\\.Load"
|
||||
- from: "cmd/netsynth/main.go"
|
||||
to: "encode/mp3.go"
|
||||
via: "encode.RunSynthesis(snapshots, outputPath, freqCfgs)"
|
||||
pattern: "encode\\.RunSynthesis.*freqCfgs"
|
||||
- from: "encode/mp3.go"
|
||||
to: "synth/bank.go"
|
||||
via: "synth.NewBank(1.0, freqCfgs) using passed-in config"
|
||||
pattern: "synth\\.NewBank.*freqCfgs"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Wire the config package into the CLI and synthesis pipeline. Add `--config` flag to Cobra, call `config.Load` at startup, change `RunSynthesis` signature to accept the merged config map, and update all call sites.
|
||||
|
||||
Purpose: Completes CFG-03 (explicit --config flag) and D-10 (RunSynthesis signature change). After this plan, the end-to-end flow works: user creates TOML -> tool loads it -> synthesis uses overridden frequencies/waveforms.
|
||||
|
||||
Output: Updated `cmd/netsynth/main.go`, `encode/mp3.go`, `encode/mp3_test.go`
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<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-01-SUMMARY.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types and contracts the executor needs. -->
|
||||
|
||||
From config/config.go (created in Plan 01):
|
||||
```go
|
||||
// Load finds, parses, validates, and merges a config file.
|
||||
// configPath is the --config flag value; empty string triggers auto-discovery.
|
||||
func Load(configPath string) (map[classify.TrafficClass]synth.FreqConfig, error)
|
||||
```
|
||||
|
||||
From encode/mp3.go (current signature to change):
|
||||
```go
|
||||
// Current:
|
||||
func RunSynthesis(snapshots []classify.WindowSnapshot, outputPath string) error
|
||||
// New:
|
||||
func RunSynthesis(snapshots []classify.WindowSnapshot, outputPath string, freqCfgs map[classify.TrafficClass]synth.FreqConfig) error
|
||||
```
|
||||
|
||||
From cmd/netsynth/main.go (existing flags pattern):
|
||||
```go
|
||||
var (
|
||||
ifaceName string
|
||||
listIfaces bool
|
||||
verbose bool
|
||||
outputPath string
|
||||
bpfFilter string
|
||||
readPath string
|
||||
)
|
||||
// Flag registration pattern:
|
||||
rootCmd.Flags().StringVar(&bpfFilter, "filter", "", "BPF filter expression")
|
||||
```
|
||||
|
||||
From synth/config.go:
|
||||
```go
|
||||
var ClassFreqConfigs = map[classify.TrafficClass]FreqConfig{ /* 14 entries */ }
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Change RunSynthesis signature and update encode tests</name>
|
||||
<files>encode/mp3.go, encode/mp3_test.go</files>
|
||||
<read_first>
|
||||
encode/mp3.go (current RunSynthesis signature and body)
|
||||
encode/mp3_test.go (current test calls to RunSynthesis)
|
||||
synth/config.go (ClassFreqConfigs, FreqConfig type)
|
||||
classify/types.go (TrafficClass type)
|
||||
</read_first>
|
||||
<action>
|
||||
**encode/mp3.go changes:**
|
||||
|
||||
1. Add `freqCfgs map[classify.TrafficClass]synth.FreqConfig` as third parameter to RunSynthesis:
|
||||
```
|
||||
func RunSynthesis(snapshots []classify.WindowSnapshot, outputPath string, freqCfgs map[classify.TrafficClass]synth.FreqConfig) error
|
||||
```
|
||||
|
||||
2. Change line 57 from:
|
||||
```
|
||||
bank := synth.NewBank(1.0, synth.ClassFreqConfigs)
|
||||
```
|
||||
to:
|
||||
```
|
||||
bank := synth.NewBank(1.0, freqCfgs)
|
||||
```
|
||||
|
||||
3. No other changes to encode/mp3.go.
|
||||
|
||||
**encode/mp3_test.go changes:**
|
||||
|
||||
4. Update `TestMP3Valid` (line 56): change `RunSynthesis(snaps, tmpPath)` to `RunSynthesis(snaps, tmpPath, synth.ClassFreqConfigs)`.
|
||||
|
||||
5. Update `TestZeroPacketError` (line 101): change `RunSynthesis([]classify.WindowSnapshot{}, tmpPath)` to `RunSynthesis([]classify.WindowSnapshot{}, tmpPath, synth.ClassFreqConfigs)`.
|
||||
|
||||
6. Update `TestZeroPacketError` (line 121): change `RunSynthesis(zeroSnaps, tmpPath2)` to `RunSynthesis(zeroSnaps, tmpPath2, synth.ClassFreqConfigs)`.
|
||||
|
||||
The `synth` import is already present in mp3_test.go.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/dev/workspace/yoloyolo && go build ./encode/... && go test ./encode/... -count=1 -run TestZeroPacketError</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- encode/mp3.go contains `func RunSynthesis(snapshots []classify.WindowSnapshot, outputPath string, freqCfgs map[classify.TrafficClass]synth.FreqConfig) error`
|
||||
- encode/mp3.go contains `synth.NewBank(1.0, freqCfgs)` (not synth.ClassFreqConfigs)
|
||||
- encode/mp3_test.go contains `RunSynthesis(snaps, tmpPath, synth.ClassFreqConfigs)`
|
||||
- encode/mp3_test.go contains `RunSynthesis([]classify.WindowSnapshot{}, tmpPath, synth.ClassFreqConfigs)`
|
||||
- encode/mp3_test.go contains `RunSynthesis(zeroSnaps, tmpPath2, synth.ClassFreqConfigs)`
|
||||
- `go build ./encode/...` exits 0
|
||||
- `go test ./encode/... -count=1 -run TestZeroPacketError` exits 0
|
||||
</acceptance_criteria>
|
||||
<done>
|
||||
RunSynthesis accepts injected config map. All encode tests updated and passing.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Add --config flag and wire config.Load into main.go</name>
|
||||
<files>cmd/netsynth/main.go</files>
|
||||
<read_first>
|
||||
cmd/netsynth/main.go (full file — flag definitions, run function, runLiveMode, runPcapMode)
|
||||
config/config.go (Load function signature)
|
||||
encode/mp3.go (updated RunSynthesis signature from Task 1)
|
||||
</read_first>
|
||||
<action>
|
||||
**cmd/netsynth/main.go changes:**
|
||||
|
||||
1. Add `configPath` to the var block (after `readPath`):
|
||||
```go
|
||||
configPath string // NEW: --config flag (CFG-03)
|
||||
```
|
||||
|
||||
2. Add import for config package in the import block:
|
||||
```go
|
||||
"github.com/netsynth/netsynth/config"
|
||||
```
|
||||
Also add import for `synth` package (needed for ClassFreqConfigs fallback reference — though config.Load handles this internally):
|
||||
No — `synth` is NOT needed in main.go. `config.Load` returns the full map. Only add `config` import.
|
||||
|
||||
3. Add flag registration in main() after the `readPath` flag line (line 44):
|
||||
```go
|
||||
rootCmd.Flags().StringVar(&configPath, "config", "", "Path to TOML config file (default: auto-discover)")
|
||||
```
|
||||
|
||||
4. In the `run` function, add config loading AFTER the BPF filter validation block (after line 80) and BEFORE output path resolution (before line 83). This is per D-11 (fail fast on config errors before capture):
|
||||
```go
|
||||
// Load config (CFG-01 through CFG-05, D-11: fail fast)
|
||||
freqCfgs, err := config.Load(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
5. The `freqCfgs` variable must be accessible in both `runLiveMode` and `runPcapMode`. Two approaches:
|
||||
- Option A: Pass freqCfgs to both functions (cleanest).
|
||||
- Option B: Store in a package-level var (simpler change).
|
||||
|
||||
Use Option A. Change signatures:
|
||||
- `runLiveMode(cmd *cobra.Command) error` -> `runLiveMode(cmd *cobra.Command, freqCfgs map[classify.TrafficClass]synth.FreqConfig) error`
|
||||
- `runPcapMode(cmd *cobra.Command) error` -> `runPcapMode(cmd *cobra.Command, freqCfgs map[classify.TrafficClass]synth.FreqConfig) error`
|
||||
|
||||
This requires adding `synth` import after all:
|
||||
```go
|
||||
"github.com/netsynth/netsynth/synth"
|
||||
```
|
||||
|
||||
Update call sites in `run()`:
|
||||
- Line 92: `return runPcapMode(cmd)` -> `return runPcapMode(cmd, freqCfgs)`
|
||||
- Line 94: `return runLiveMode(cmd)` -> `return runLiveMode(cmd, freqCfgs)`
|
||||
|
||||
6. In `runLiveMode`, change the RunSynthesis call (line 147):
|
||||
From: `encode.RunSynthesis(collectedSnapshots, outputPath)`
|
||||
To: `encode.RunSynthesis(collectedSnapshots, outputPath, freqCfgs)`
|
||||
|
||||
7. In `runPcapMode`, change the RunSynthesis call (line 215):
|
||||
From: `encode.RunSynthesis(collectedSnapshots, outputPath)`
|
||||
To: `encode.RunSynthesis(collectedSnapshots, outputPath, freqCfgs)`
|
||||
|
||||
After all changes, run `go build ./cmd/netsynth/...` to verify compilation.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/dev/workspace/yoloyolo && go build ./cmd/netsynth/... && go vet ./cmd/netsynth/... && go test ./... -count=1 2>&1 | tail -20</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- cmd/netsynth/main.go contains `configPath string`
|
||||
- cmd/netsynth/main.go contains `rootCmd.Flags().StringVar(&configPath, "config", ""`
|
||||
- cmd/netsynth/main.go contains `config.Load(configPath)`
|
||||
- cmd/netsynth/main.go contains `"github.com/netsynth/netsynth/config"` in imports
|
||||
- cmd/netsynth/main.go contains `encode.RunSynthesis(collectedSnapshots, outputPath, freqCfgs)` (two occurrences — one in runLiveMode, one in runPcapMode)
|
||||
- cmd/netsynth/main.go contains `runLiveMode(cmd, freqCfgs)` and `runPcapMode(cmd, freqCfgs)`
|
||||
- `go build ./cmd/netsynth/...` exits 0
|
||||
- `go vet ./cmd/netsynth/...` exits 0
|
||||
- `go test ./... -count=1` exits 0 (full suite green)
|
||||
</acceptance_criteria>
|
||||
<done>
|
||||
--config flag registered in Cobra. config.Load called at startup before capture. Merged config map flows through to RunSynthesis in both live and pcap modes. Full test suite passes.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `go build ./...` compiles entire project
|
||||
- `go test ./... -count=1` all tests pass
|
||||
- `go vet ./...` no issues
|
||||
- `./netsynth --help` shows `--config` flag in output
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- --config flag appears in CLI help output (CFG-03)
|
||||
- Explicit --config with missing file produces error before capture (CFG-03)
|
||||
- RunSynthesis uses injected config map, not hardcoded ClassFreqConfigs (D-10)
|
||||
- Full test suite passes including encode and config package tests
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/06-config-package-and-sound-overrides/06-02-SUMMARY.md`
|
||||
</output>
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
---
|
||||
phase: 06-config-package-and-sound-overrides
|
||||
plan: 02
|
||||
subsystem: cmd/encode
|
||||
tags: [cli, config, RunSynthesis, dependency-injection, cobra]
|
||||
|
||||
# Dependency graph
|
||||
requires:
|
||||
- phase: 06-01
|
||||
provides: config.Load(configPath string) returns map[classify.TrafficClass]synth.FreqConfig
|
||||
- phase: 05-waveform-types-and-bank-decoupling
|
||||
provides: NewBank(tau, cfgs) with injected config map, FreqConfig.WaveformType
|
||||
|
||||
provides:
|
||||
- --config flag in CLI (CFG-03)
|
||||
- config.Load called at startup before capture (D-11 fail fast)
|
||||
- RunSynthesis(snapshots, outputPath, freqCfgs) with injected config map (D-10)
|
||||
- Merged config flows end-to-end: TOML file -> config.Load -> RunSynthesis -> NewBank
|
||||
|
||||
affects:
|
||||
- encode/mp3.go (RunSynthesis signature changed)
|
||||
- cmd/netsynth/main.go (--config flag, config.Load, pass freqCfgs through pipeline)
|
||||
|
||||
# Tech tracking
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "Dependency injection: config map flows from main() through runLiveMode/runPcapMode to RunSynthesis to NewBank"
|
||||
- "Fail-fast config loading: config.Load called after BPF validation, before capture starts (D-11)"
|
||||
- "Explicit configPath string var for --config flag, empty string triggers auto-discovery"
|
||||
|
||||
key-files:
|
||||
created: []
|
||||
modified:
|
||||
- encode/mp3.go
|
||||
- encode/mp3_test.go
|
||||
- cmd/netsynth/main.go
|
||||
|
||||
key-decisions:
|
||||
- "Option A for freqCfgs propagation: pass as parameter to runLiveMode/runPcapMode rather than package-level var — explicit data flow, easier to test"
|
||||
- "config.Load called before output path resolution — config errors abort before any state changes"
|
||||
|
||||
patterns-established:
|
||||
- "Config map injected at call boundary (main -> run -> runLiveMode/runPcapMode -> RunSynthesis -> NewBank)"
|
||||
|
||||
requirements-completed: [CFG-03]
|
||||
|
||||
# Metrics
|
||||
duration: 2min
|
||||
completed: 2026-03-26
|
||||
---
|
||||
|
||||
# Phase 6 Plan 02: CLI Config Wiring Summary
|
||||
|
||||
**--config flag added to Cobra, config.Load wired at startup, RunSynthesis signature changed to accept injected freqCfgs map — end-to-end config flow from TOML file to synthesis**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** ~2 min
|
||||
- **Started:** 2026-03-26T20:01:59Z
|
||||
- **Completed:** 2026-03-26T20:04:27Z
|
||||
- **Tasks:** 2
|
||||
- **Files modified:** 3 (encode/mp3.go, encode/mp3_test.go, cmd/netsynth/main.go)
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- Changed `RunSynthesis` third parameter: accepts `freqCfgs map[classify.TrafficClass]synth.FreqConfig` (D-10)
|
||||
- Updated all 3 RunSynthesis call sites in encode tests to pass `synth.ClassFreqConfigs`
|
||||
- Added `configPath string` var and `--config` flag registration in Cobra (CFG-03)
|
||||
- Imported `config` and `synth` packages into cmd/netsynth/main.go
|
||||
- Wired `config.Load(configPath)` into `run()` after BPF validation, before capture (D-11)
|
||||
- Changed `runLiveMode` and `runPcapMode` signatures to accept `freqCfgs` parameter
|
||||
- Updated both `encode.RunSynthesis` call sites to pass `freqCfgs`
|
||||
- Full test suite passes: 7 packages, all green
|
||||
|
||||
## Task Commits
|
||||
|
||||
1. **Task 1** - `3dfcbbe` feat(06-02): add freqCfgs parameter to RunSynthesis
|
||||
2. **Task 2** - `413cceb` feat(06-02): wire --config flag and config.Load into CLI pipeline
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `encode/mp3.go` - RunSynthesis now accepts `freqCfgs map[classify.TrafficClass]synth.FreqConfig`; uses `freqCfgs` in `synth.NewBank(1.0, freqCfgs)` call
|
||||
- `encode/mp3_test.go` - Updated 3 RunSynthesis calls to pass `synth.ClassFreqConfigs` as third arg
|
||||
- `cmd/netsynth/main.go` - `configPath` var, `--config` flag, `config` and `synth` imports, `config.Load` call, updated function signatures, updated RunSynthesis calls
|
||||
|
||||
## Decisions Made
|
||||
|
||||
- **Option A for freqCfgs propagation**: Pass config map as function parameter to `runLiveMode`/`runPcapMode` rather than storing in a package-level variable. Cleaner data flow, functions remain testable in isolation.
|
||||
- **config.Load position in run()**: Called after BPF filter validation, before output path resolution and capture start. Config errors abort immediately before any I/O begins (D-11).
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
None - plan executed exactly as written. The worktree required a rebase onto local master to include phase 05 bank-decoupling code (NewBank 2-arg signature) and phase 06-01 config package before implementation could begin — this is expected prerequisite resolution, not a deviation.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
- Worktree was based on origin/master (commit 41e2278, pre-phase-05). Rebased onto local master (936aeea) to get WaveformType, 2-arg NewBank, and config package. No code conflicts.
|
||||
|
||||
## User Setup Required
|
||||
|
||||
None.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
- Full end-to-end config flow is wired: user creates netsynth.toml -> `--config` passes path -> `config.Load` merges -> `RunSynthesis` uses merged map -> `NewBank` synthesizes with custom frequencies/waveforms
|
||||
- Phase 06-03 (if any) can build on this wired pipeline for additional config features
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
- FOUND: encode/mp3.go — contains `func RunSynthesis(snapshots []classify.WindowSnapshot, outputPath string, freqCfgs map[classify.TrafficClass]synth.FreqConfig) error`
|
||||
- FOUND: encode/mp3_test.go — contains `RunSynthesis(snaps, tmpPath, synth.ClassFreqConfigs)`
|
||||
- FOUND: cmd/netsynth/main.go — contains `configPath string`, `config.Load(configPath)`, `runLiveMode(cmd, freqCfgs)`
|
||||
- FOUND: 3dfcbbe (Task 1 commit)
|
||||
- FOUND: 413cceb (Task 2 commit)
|
||||
|
||||
---
|
||||
*Phase: 06-config-package-and-sound-overrides*
|
||||
*Completed: 2026-03-26*
|
||||
@@ -0,0 +1,111 @@
|
||||
# Phase 6: Config Package and Sound Overrides - Context
|
||||
|
||||
**Gathered:** 2026-03-26
|
||||
**Status:** Ready for planning
|
||||
|
||||
<domain>
|
||||
## Phase Boundary
|
||||
|
||||
Add a TOML-based configuration system that lets users override frequency and waveform per traffic class, with auto-discovery from standard paths, explicit `--config` flag, partial override semantics (only specified fields change), and strict unknown-key validation. Wire the merged config into the synthesis pipeline via the injection seam created in Phase 5.
|
||||
|
||||
Requirements covered: CFG-01 through CFG-05.
|
||||
|
||||
</domain>
|
||||
|
||||
<decisions>
|
||||
## Implementation Decisions
|
||||
|
||||
### TOML Schema Design
|
||||
- **D-01:** Use keyed TOML tables `[sounds.<classname>]` for per-class overrides. Each table supports `frequency` (float64, Hz) and `waveform` (string: "sine", "square", "sawtooth", "triangle"). Class names match `classify.TrafficClass` string values (e.g., `[sounds.ICMP]`, `[sounds.HTTPS]`).
|
||||
- **D-02:** Top-level structure is flat — no deeply nested hierarchies. Future phases (custom rules) will add `[[rules]]` array-of-tables at the top level.
|
||||
|
||||
### Config Merge Semantics
|
||||
- **D-03:** Per-field overlay merge — only fields explicitly set in TOML override defaults. Unspecified fields retain their built-in values. For example, setting only `frequency` for ICMP leaves its waveform and harmonics unchanged. This satisfies CFG-04 (partial override without replicating entire config).
|
||||
- **D-04:** Merge produces a `map[classify.TrafficClass]FreqConfig` that is passed to `synth.NewBank()` via the injection seam from Phase 5. The default map is `synth.ClassFreqConfigs`.
|
||||
|
||||
### Auto-Discovery and Precedence
|
||||
- **D-05:** Discovery order (most-specific wins): `--config <path>` > `./netsynth.toml` > `~/.config/netsynth/config.toml`. If `--config` is specified and the file does not exist, exit with a clear error before capture begins (CFG-03). If no config is found via auto-discovery, proceed silently with defaults (CFG-02).
|
||||
- **D-06:** Only one config file is loaded — no multi-file merge. The first found in precedence order wins entirely.
|
||||
|
||||
### Validation and Error Reporting
|
||||
- **D-07:** Unknown keys cause an immediate startup error naming the unrecognized key (CFG-05). Use TOML strict decoding to detect unknown keys. Suggest the closest valid key name if edit distance is small (nice-to-have, Claude's discretion on implementation).
|
||||
- **D-08:** Type mismatches (e.g., `frequency = "not a number"`) produce a clear error with field name and expected type, before capture begins.
|
||||
- **D-09:** Unknown class names in `[sounds.<classname>]` produce a warning (not error) — this prepares for Phase 7 where user-defined class names are valid.
|
||||
|
||||
### Pipeline Wiring
|
||||
- **D-10:** `encode.RunSynthesis` signature changes to accept the merged config map (or loads config internally). The `--config` flag is added to the Cobra root command in `cmd/netsynth/main.go`.
|
||||
- **D-11:** Config loading happens once at startup, before any capture begins — fail fast on all config errors.
|
||||
|
||||
### Claude's Discretion
|
||||
- TOML library choice (BurntSushi/toml vs pelletier/go-toml) — researcher should evaluate both
|
||||
- Whether to create a dedicated `config` package or keep loading in `cmd/netsynth`
|
||||
- Waveform string-to-WaveformType mapping implementation details
|
||||
- Edit distance algorithm for typo suggestions (or skip if complexity isn't justified)
|
||||
|
||||
</decisions>
|
||||
|
||||
<canonical_refs>
|
||||
## Canonical References
|
||||
|
||||
**Downstream agents MUST read these before planning or implementing.**
|
||||
|
||||
### Injection Seam (Phase 5 output)
|
||||
- `synth/bank.go` — `NewBank(tau float64, cfgs map[classify.TrafficClass]FreqConfig)` — the injection point for merged config
|
||||
- `synth/config.go` — `ClassFreqConfigs` default map, `FreqConfig` struct with `WaveformType` field, `WaveformPresetHarmonics()` function
|
||||
- `encode/mp3.go` — `RunSynthesis()` calls `synth.NewBank(1.0, synth.ClassFreqConfigs)` — the call site to modify
|
||||
|
||||
### CLI Entry Point
|
||||
- `cmd/netsynth/main.go` — Cobra command setup, flag definitions, `run()` function that dispatches to live/pcap modes
|
||||
|
||||
### Requirements
|
||||
- `.planning/REQUIREMENTS.md` — CFG-01 through CFG-05 acceptance criteria
|
||||
|
||||
### Prior Context
|
||||
- `.planning/phases/05-waveform-types-and-bank-decoupling/05-CONTEXT.md` — Phase 5 decisions (D-02 WaveformType, D-05 bank injection seam)
|
||||
|
||||
No external specs — requirements fully captured in decisions above.
|
||||
|
||||
</canonical_refs>
|
||||
|
||||
<code_context>
|
||||
## Existing Code Insights
|
||||
|
||||
### Reusable Assets
|
||||
- `synth.ClassFreqConfigs` — Default config map (14 entries), serves as base for merge
|
||||
- `synth.WaveformType` enum — Maps to TOML waveform strings (sine/square/sawtooth/triangle)
|
||||
- `synth.NewBank(tau, cfgs)` — Already accepts injected config map (Phase 5)
|
||||
- `classify.TrafficClass` (string type) — Keys for config map, matches TOML section names
|
||||
- `classify.AllClasses()` — Returns all 14 built-in class names for validation
|
||||
|
||||
### Established Patterns
|
||||
- Cobra for CLI flags — add `--config` flag in same pattern as existing flags
|
||||
- `encode.RunSynthesis` is the single call site for synthesis — modification point is narrow
|
||||
- Package-level vars (`ClassFreqConfigs`, `DefaultRules`) serve as defaults — config system overlays on top
|
||||
|
||||
### Integration Points
|
||||
- `cmd/netsynth/main.go:run()` — Config loading inserts between flag parsing and capture start
|
||||
- `encode.RunSynthesis()` — Must receive merged config map (currently hardcoded to `synth.ClassFreqConfigs`)
|
||||
- `synth.FreqConfig.WaveformType` field — Set from TOML waveform string after parsing
|
||||
|
||||
</code_context>
|
||||
|
||||
<specifics>
|
||||
## Specific Ideas
|
||||
|
||||
No specific requirements — standard TOML config pattern with partial merge semantics.
|
||||
|
||||
</specifics>
|
||||
|
||||
<deferred>
|
||||
## Deferred Ideas
|
||||
|
||||
- `--print-config` command (CFG-06) — scoped to Phase 7
|
||||
- Custom classification rules (`[[rules]]` TOML blocks) — scoped to Phase 7
|
||||
- Config hot-reload — explicitly out of scope per REQUIREMENTS.md
|
||||
|
||||
</deferred>
|
||||
|
||||
---
|
||||
|
||||
*Phase: 06-config-package-and-sound-overrides*
|
||||
*Context gathered: 2026-03-26*
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
# Phase 6: Config Package and Sound Overrides - Discussion Log
|
||||
|
||||
> **Audit trail only.** Do not use as input to planning, research, or execution agents.
|
||||
> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
|
||||
|
||||
**Date:** 2026-03-26
|
||||
**Phase:** 06-config-package-and-sound-overrides
|
||||
**Areas discussed:** TOML structure, Config merge, Auto-discovery precedence, Error reporting
|
||||
**Mode:** --auto (all areas auto-selected, recommended defaults chosen)
|
||||
|
||||
---
|
||||
|
||||
## TOML Structure
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Keyed table `[sounds.<classname>]` | Natural TOML pattern, matches traffic class names | ✓ |
|
||||
| Flat key-value pairs | Simpler but doesn't scale to per-class overrides | |
|
||||
| Nested `[sounds.<classname>.audio]` | Unnecessary nesting depth | |
|
||||
|
||||
**User's choice:** [auto] Keyed table `[sounds.<classname>]` (recommended default)
|
||||
**Notes:** Matches classify.TrafficClass string values directly. Supports `frequency` and `waveform` fields per class.
|
||||
|
||||
---
|
||||
|
||||
## Config Merge Semantics
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Per-field overlay | Only specified fields override defaults (CFG-04) | ✓ |
|
||||
| Full section replace | Setting any field in a class replaces all fields | |
|
||||
| Deep merge with arrays | Overkill for flat config structure | |
|
||||
|
||||
**User's choice:** [auto] Per-field overlay (recommended default)
|
||||
**Notes:** Satisfies CFG-04 requirement. User sets one field, everything else keeps defaults.
|
||||
|
||||
---
|
||||
|
||||
## Auto-Discovery Precedence
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Local > user > flag | Most-specific wins: --config > ./netsynth.toml > ~/.config/ | ✓ |
|
||||
| Flag only | Simpler but no auto-discovery (violates CFG-02) | |
|
||||
| Multi-file merge | Load and merge all found configs | |
|
||||
|
||||
**User's choice:** [auto] Local > user-level > flag (recommended default)
|
||||
**Notes:** Standard CLI convention. Only one file loaded — no multi-file merge complexity.
|
||||
|
||||
---
|
||||
|
||||
## Error Reporting
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Fail-fast with key name + suggestion | Exit at startup, name the bad key (CFG-05) | ✓ |
|
||||
| Warning and continue | Tolerant but hides mistakes | |
|
||||
| Strict with no suggestions | Simpler but less helpful | |
|
||||
|
||||
**User's choice:** [auto] Fail-fast with key name and optional typo suggestion (recommended default)
|
||||
**Notes:** Matches CFG-05 requirement. Unknown class names are warnings (not errors) to prepare for Phase 7.
|
||||
|
||||
---
|
||||
|
||||
## Claude's Discretion
|
||||
|
||||
- TOML library choice
|
||||
- Package organization (dedicated `config` package vs inline)
|
||||
- Waveform string mapping implementation
|
||||
- Edit distance for typo suggestions
|
||||
|
||||
## Deferred Ideas
|
||||
|
||||
- `--print-config` (CFG-06) — Phase 7
|
||||
- Custom rules `[[rules]]` — Phase 7
|
||||
+590
@@ -0,0 +1,590 @@
|
||||
# Phase 6: Config Package and Sound Overrides - Research
|
||||
|
||||
**Researched:** 2026-03-26
|
||||
**Domain:** Go TOML config loading, partial merge semantics, CLI flag wiring
|
||||
**Confidence:** HIGH
|
||||
|
||||
---
|
||||
|
||||
<user_constraints>
|
||||
## User Constraints (from CONTEXT.md)
|
||||
|
||||
### Locked Decisions
|
||||
|
||||
- **D-01:** Use keyed TOML tables `[sounds.<classname>]` for per-class overrides. Each table supports `frequency` (float64, Hz) and `waveform` (string: "sine", "square", "sawtooth", "triangle"). Class names match `classify.TrafficClass` string values (e.g., `[sounds.ICMP]`, `[sounds.HTTPS]`).
|
||||
- **D-02:** Top-level structure is flat — no deeply nested hierarchies. Future phases (custom rules) will add `[[rules]]` array-of-tables at the top level.
|
||||
- **D-03:** Per-field overlay merge — only fields explicitly set in TOML override defaults. Unspecified fields retain their built-in values. For example, setting only `frequency` for ICMP leaves its waveform and harmonics unchanged. This satisfies CFG-04 (partial override without replicating entire config).
|
||||
- **D-04:** Merge produces a `map[classify.TrafficClass]FreqConfig` that is passed to `synth.NewBank()` via the injection seam from Phase 5. The default map is `synth.ClassFreqConfigs`.
|
||||
- **D-05:** Discovery order (most-specific wins): `--config <path>` > `./netsynth.toml` > `~/.config/netsynth/config.toml`. If `--config` is specified and the file does not exist, exit with a clear error before capture begins (CFG-03). If no config is found via auto-discovery, proceed silently with defaults (CFG-02).
|
||||
- **D-06:** Only one config file is loaded — no multi-file merge. The first found in precedence order wins entirely.
|
||||
- **D-07:** Unknown keys cause an immediate startup error naming the unrecognized key (CFG-05). Use TOML strict decoding to detect unknown keys. Suggest the closest valid key name if edit distance is small (nice-to-have, Claude's discretion on implementation).
|
||||
- **D-08:** Type mismatches (e.g., `frequency = "not a number"`) produce a clear error with field name and expected type, before capture begins.
|
||||
- **D-09:** Unknown class names in `[sounds.<classname>]` produce a warning (not error) — this prepares for Phase 7 where user-defined class names are valid.
|
||||
- **D-10:** `encode.RunSynthesis` signature changes to accept the merged config map (or loads config internally). The `--config` flag is added to the Cobra root command in `cmd/netsynth/main.go`.
|
||||
- **D-11:** Config loading happens once at startup, before any capture begins — fail fast on all config errors.
|
||||
|
||||
### Claude's Discretion
|
||||
|
||||
- TOML library choice (BurntSushi/toml vs pelletier/go-toml) — researcher should evaluate both
|
||||
- Whether to create a dedicated `config` package or keep loading in `cmd/netsynth`
|
||||
- Waveform string-to-WaveformType mapping implementation details
|
||||
- Edit distance algorithm for typo suggestions (or skip if complexity isn't justified)
|
||||
|
||||
### Deferred Ideas (OUT OF SCOPE)
|
||||
|
||||
- `--print-config` command (CFG-06) — scoped to Phase 7
|
||||
- Custom classification rules (`[[rules]]` TOML blocks) — scoped to Phase 7
|
||||
- Config hot-reload — explicitly out of scope per REQUIREMENTS.md
|
||||
|
||||
</user_constraints>
|
||||
|
||||
<phase_requirements>
|
||||
## Phase Requirements
|
||||
|
||||
| ID | Description | Research Support |
|
||||
|----|-------------|------------------|
|
||||
| CFG-01 | User can create a TOML config file that overrides default sound mappings | `[sounds.<classname>]` table pattern decodes into `map[string]SoundOverride`; per-field merge into `synth.ClassFreqConfigs` clone |
|
||||
| CFG-02 | Tool auto-discovers config from `./netsynth.toml` or `~/.config/netsynth/config.toml` (silent if absent) | `os.Stat` probe + `os.UserConfigDir()` for XDG path; `errors.Is(err, fs.ErrNotExist)` for silent miss |
|
||||
| CFG-03 | User can specify an explicit config path via `--config` flag (error if file missing) | Cobra `StringVar` flag; fail-fast `os.Stat` check returns error before capture begins |
|
||||
| CFG-04 | User can override individual values without replicating the entire default config (partial override) | Pointer fields (`*float64`, `*string`) in the TOML decode struct allow distinguishing "explicitly zero" from "not set"; overlay merge copies only non-nil fields |
|
||||
| CFG-05 | Unknown keys in config file produce a clear error with the typo'd key name | BurntSushi/toml `MetaData.Undecoded()` returns unmatched keys after decode; format as error message |
|
||||
|
||||
</phase_requirements>
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Phase 6 adds a `config` package responsible for loading a TOML config file, validating it, and merging it over the `synth.ClassFreqConfigs` default map. The merge output is a `map[classify.TrafficClass]synth.FreqConfig` that is handed to `synth.NewBank()` — the injection seam already exists from Phase 5.
|
||||
|
||||
The core technical challenge is **partial override semantics**: a user who sets only `frequency` for ICMP must not accidentally clear its waveform. This requires the decode struct to use pointer fields (`*float64`, `*string`) so that absent keys remain `nil` at decode time. The merge loop then only copies non-nil values over the defaults.
|
||||
|
||||
Unknown-key detection uses BurntSushi/toml v1.6.0's `MetaData.Undecoded()` method, which is reliable because it operates on the actual set of keys the parser traversed. The alternative (pelletier/go-toml v2.3.0's `DisallowUnknownFields`) is also viable but adds a dependency with a different API surface and returns human-formatted error strings rather than structured key lists — less useful for the "suggest closest valid key" nice-to-have.
|
||||
|
||||
**Primary recommendation:** Use `github.com/BurntSushi/toml` v1.6.0. Use a dedicated `config` package. Implement partial merge with pointer fields in the TOML decode struct.
|
||||
|
||||
## Standard Stack
|
||||
|
||||
### Core
|
||||
|
||||
| Library | Version | Purpose | Why Standard |
|
||||
|---------|---------|---------|--------------|
|
||||
| `github.com/BurntSushi/toml` | v1.6.0 | TOML parsing and MetaData for unknown-key detection | Simpler API than pelletier v2; `Undecoded()` returns structured `[]Key` (not formatted error strings); `DecodeFile()` is a one-liner; v1.6.0 published December 2025 |
|
||||
|
||||
### Supporting
|
||||
|
||||
| Library | Version | Purpose | When to Use |
|
||||
|---------|---------|---------|-------------|
|
||||
| `os` (stdlib) | Go 1.24 | File existence checks, `UserConfigDir()` for XDG path | Always — no external dependency needed for discovery logic |
|
||||
| `errors`/`fs` (stdlib) | Go 1.24 | `errors.Is(err, fs.ErrNotExist)` for silent-miss on auto-discovery | Always |
|
||||
|
||||
### Alternatives Considered
|
||||
|
||||
| Instead of | Could Use | Tradeoff |
|
||||
|------------|-----------|----------|
|
||||
| `BurntSushi/toml v1.6.0` | `pelletier/go-toml v2.3.0` | go-toml has `DisallowUnknownFields()` built-in (cleaner API) but returns `StrictMissingError` with a formatted string — harder to extract just the key name for a "did you mean?" suggestion. BurntSushi returns `[]toml.Key` which is structured. For this use case, BurntSushi is easier to work with. |
|
||||
| Pointer fields for partial override | Separate "is-set" booleans | Pointer fields are idiomatic in Go for "optional" semantics. Booleans add field count and are error-prone. |
|
||||
| Dedicated `config` package | Inline in `cmd/netsynth` | A `config` package makes the loader independently testable without a Cobra dependency. Given the complexity (validation, merge, discovery), a separate package is justified. |
|
||||
|
||||
**Installation:**
|
||||
```bash
|
||||
go get github.com/BurntSushi/toml@v1.6.0
|
||||
```
|
||||
|
||||
**Version verification (confirmed 2026-03-26):**
|
||||
```
|
||||
github.com/BurntSushi/toml v1.6.0 (December 18, 2025)
|
||||
github.com/pelletier/go-toml/v2 v2.3.0 (March 24, 2026 — alternative)
|
||||
```
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
### Recommended Project Structure
|
||||
|
||||
```
|
||||
config/
|
||||
├── config.go # Load(), Merge(), Validate() — public API
|
||||
└── config_test.go # table-driven tests for all CFG requirements
|
||||
```
|
||||
|
||||
The `config` package has one exported function signature the planner cares about:
|
||||
|
||||
```go
|
||||
// Load finds, parses, validates, and merges a config file.
|
||||
// configPath is the --config flag value; empty string triggers auto-discovery.
|
||||
// Returns the merged FreqConfig map (defaults + overrides) ready for synth.NewBank.
|
||||
// Returns an error on: file-not-found when --config is explicit, parse errors,
|
||||
// unknown keys, type mismatches. Returns no error (uses defaults) when no config
|
||||
// is found during auto-discovery.
|
||||
func Load(configPath string) (map[classify.TrafficClass]synth.FreqConfig, error)
|
||||
```
|
||||
|
||||
### Pattern 1: TOML Decode Struct with Pointer Fields
|
||||
|
||||
**What:** The TOML config file maps to a Go struct where every overridable field is a pointer. `nil` means "not set by user"; non-nil means "user explicitly specified this value."
|
||||
|
||||
**When to use:** Whenever you need to distinguish "field absent from config" from "field set to zero value" — mandatory for partial override semantics (CFG-04).
|
||||
|
||||
```go
|
||||
// Source: BurntSushi/toml documentation + partial-override pattern
|
||||
// config/config.go
|
||||
|
||||
// SoundOverride holds optional per-class sound parameters from TOML.
|
||||
// Pointer fields: nil = not set (keep default), non-nil = user override.
|
||||
type SoundOverride struct {
|
||||
Frequency *float64 `toml:"frequency"`
|
||||
Waveform *string `toml:"waveform"`
|
||||
}
|
||||
|
||||
// rawConfig is the top-level TOML decode target.
|
||||
type rawConfig struct {
|
||||
Sounds map[string]SoundOverride `toml:"sounds"`
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: Unknown-Key Detection with MetaData.Undecoded()
|
||||
|
||||
**What:** After decoding, check `md.Undecoded()` for any keys in the TOML file that did not map to a field in the decode struct. Return an error naming the first unrecognized key.
|
||||
|
||||
**When to use:** Required for CFG-05. Also the mechanism to detect field-level typos within a `[sounds.ICMP]` block (e.g., `frequncy` vs `frequency`).
|
||||
|
||||
```go
|
||||
// Source: pkg.go.dev/github.com/BurntSushi/toml
|
||||
// config/config.go
|
||||
|
||||
func parse(path string) (rawConfig, error) {
|
||||
var raw rawConfig
|
||||
md, err := toml.DecodeFile(path, &raw)
|
||||
if err != nil {
|
||||
return raw, fmt.Errorf("config parse error: %w", err)
|
||||
}
|
||||
if undecoded := md.Undecoded(); len(undecoded) > 0 {
|
||||
// undecoded[0] is a toml.Key ([]string); join for human-readable path
|
||||
keyPath := strings.Join(undecoded[0], ".")
|
||||
return raw, fmt.Errorf("config: unknown key %q — check spelling", keyPath)
|
||||
}
|
||||
return raw, nil
|
||||
}
|
||||
```
|
||||
|
||||
**IMPORTANT NOTE on nested map + Undecoded():** When the decode struct uses `map[string]SoundOverride` for `[sounds]`, the TOML library cannot know what map keys are "valid" — all string keys are valid map keys. This means `Undecoded()` will NOT catch a misspelled class name like `[sounds.ICMP_typo]` (it IS decoded, just into a wrong map key). However, `Undecoded()` WILL catch field-level typos within a class block like `[sounds.ICMP]` with `frequncy = 440` because `frequncy` doesn't match any `SoundOverride` field. Class-name validation is handled separately in the merge step (D-09: log a warning for unknown class names).
|
||||
|
||||
### Pattern 3: Per-Field Overlay Merge
|
||||
|
||||
**What:** Iterate over the default `ClassFreqConfigs` map, copy it, then for each entry found in the TOML overrides, copy only the non-nil pointer fields into the working copy.
|
||||
|
||||
**When to use:** This is the CFG-04 implementation. Must run after parse and validation.
|
||||
|
||||
```go
|
||||
// config/config.go
|
||||
|
||||
func merge(
|
||||
defaults map[classify.TrafficClass]synth.FreqConfig,
|
||||
overrides map[string]SoundOverride,
|
||||
) map[classify.TrafficClass]synth.FreqConfig {
|
||||
// Deep-copy defaults
|
||||
result := make(map[classify.TrafficClass]synth.FreqConfig, len(defaults))
|
||||
for k, v := range defaults {
|
||||
result[k] = v
|
||||
}
|
||||
|
||||
for className, override := range overrides {
|
||||
class := classify.TrafficClass(className)
|
||||
cfg, known := result[class]
|
||||
if !known {
|
||||
// D-09: unknown class name = warning, not error (Phase 7 may define it)
|
||||
fmt.Fprintf(os.Stderr, "Warning: config: unknown class %q (ignored)\n", className)
|
||||
continue
|
||||
}
|
||||
if override.Frequency != nil {
|
||||
cfg.BaseHz = *override.Frequency
|
||||
// When frequency changes, regenerate harmonics if a waveform preset is active
|
||||
if cfg.WaveformType != synth.WaveformCustom {
|
||||
cfg.Harmonics = synth.WaveformPresetHarmonics(cfg.WaveformType, cfg.BaseHz, synth.SampleRate)
|
||||
}
|
||||
}
|
||||
if override.Waveform != nil {
|
||||
wt, err := parseWaveform(*override.Waveform)
|
||||
if err != nil {
|
||||
// Validation catches this before merge; this is a safety guard
|
||||
continue
|
||||
}
|
||||
cfg.WaveformType = wt
|
||||
cfg.Harmonics = synth.WaveformPresetHarmonics(wt, cfg.BaseHz, synth.SampleRate)
|
||||
}
|
||||
result[class] = cfg
|
||||
}
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 4: Waveform String-to-Type Mapping
|
||||
|
||||
**What:** A simple switch converts the TOML `waveform` string to `synth.WaveformType`. Validation happens before merge.
|
||||
|
||||
```go
|
||||
// config/config.go
|
||||
|
||||
var validWaveforms = map[string]synth.WaveformType{
|
||||
"sine": synth.WaveformSine,
|
||||
"square": synth.WaveformSquare,
|
||||
"sawtooth": synth.WaveformSawtooth,
|
||||
"triangle": synth.WaveformTriangle,
|
||||
}
|
||||
|
||||
func 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, ", "))
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 5: Auto-Discovery with os.UserConfigDir
|
||||
|
||||
**What:** Check paths in precedence order. Return the path of the first file found, or `""` (empty) if none found. Never log anything for a missing auto-discovered file.
|
||||
|
||||
```go
|
||||
// config/config.go
|
||||
|
||||
func discoverPath() string {
|
||||
// 1. Working directory
|
||||
if _, err := os.Stat("netsynth.toml"); err == nil {
|
||||
return "netsynth.toml"
|
||||
}
|
||||
// 2. XDG config dir
|
||||
dir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
p := filepath.Join(dir, "netsynth", "config.toml")
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
return p
|
||||
}
|
||||
return ""
|
||||
}
|
||||
```
|
||||
|
||||
`os.UserConfigDir()` returns `$XDG_CONFIG_HOME` or `$HOME/.config` on Linux (Go stdlib, no extra dependency). Confirmed by Go source: returns `$XDG_CONFIG_HOME` if set, else `$HOME/.config` on Unix.
|
||||
|
||||
### Pattern 6: encode.RunSynthesis Signature Change
|
||||
|
||||
**What:** `RunSynthesis` currently calls `synth.NewBank(1.0, synth.ClassFreqConfigs)` hardcoded. Phase 6 changes the signature to accept the merged config map.
|
||||
|
||||
The simplest approach: pass the merged config map as a parameter (rather than loading config inside `encode`). This keeps `encode` unaware of config loading and makes testing easier.
|
||||
|
||||
```go
|
||||
// encode/mp3.go — updated signature
|
||||
func RunSynthesis(
|
||||
snapshots []classify.WindowSnapshot,
|
||||
outputPath string,
|
||||
freqCfgs map[classify.TrafficClass]synth.FreqConfig,
|
||||
) error {
|
||||
// ...
|
||||
bank := synth.NewBank(1.0, freqCfgs) // was: synth.ClassFreqConfigs
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Caller in `cmd/netsynth/main.go` passes the result of `config.Load(configPath)`.
|
||||
|
||||
### Anti-Patterns to Avoid
|
||||
|
||||
- **Decode into `map[string]interface{}`:** Loses type safety, makes unknown-field detection harder, requires runtime type assertions. Use typed structs.
|
||||
- **Load config inside `encode` package:** Couples audio encoding to config I/O; breaks test isolation. Config loading belongs in `cmd/netsynth/main.go` (calls `config.Load`) or a dedicated `config` package.
|
||||
- **Validate waveform strings after merge:** Validate before merging so the error is caught at startup (D-11), not silently ignored.
|
||||
- **Deep-copy using `=` assignment on map values:** `synth.FreqConfig` contains a `[]HarmonicDef` slice; a simple struct copy shares the underlying array. Use an explicit copy of the slice if you mutate `Harmonics` during merge. (The merge code above reconstructs harmonics from the preset, so this is safe — but important to be aware of.)
|
||||
|
||||
## Don't Hand-Roll
|
||||
|
||||
| Problem | Don't Build | Use Instead | Why |
|
||||
|---------|-------------|-------------|-----|
|
||||
| TOML parsing | Custom parser | `BurntSushi/toml` v1.6.0 | TOML 1.1 compliance, error messages, datetime support, tested at scale |
|
||||
| Unknown-key detection | Post-parse key comparison | `md.Undecoded()` from BurntSushi | Already built into the library; handles nested paths correctly |
|
||||
| XDG config path | Manual `$HOME/.config` string concat | `os.UserConfigDir()` stdlib | Handles `$XDG_CONFIG_HOME` override correctly, platform-portable |
|
||||
|
||||
**Key insight:** The partial-override merge logic is the one piece that must be written from scratch — no library does "overlay a sparse map of optional overrides over a typed defaults map." But it is ~20 lines of straightforward Go.
|
||||
|
||||
## Runtime State Inventory
|
||||
|
||||
Step 2.5 SKIPPED — this is a new feature addition, not a rename/refactor/migration phase. No runtime state is being renamed or migrated.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Pitfall 1: Undecoded() Does Not Catch Unknown Class Names
|
||||
|
||||
**What goes wrong:** Developer assumes `md.Undecoded()` will catch `[sounds.ICMP_TYPO]` as an unknown key and provide CFG-05 coverage for class-name typos.
|
||||
|
||||
**Why it happens:** `map[string]SoundOverride` decodes any string as a valid map key — the TOML parser has no way to know which class names are valid. `Undecoded()` only catches keys that don't match ANY field (struct field name, map key, or slice element). Since all map keys are valid, no class name is "undecodeable."
|
||||
|
||||
**How to avoid:** Separate the two concerns. Field-level unknown keys (e.g., `frequncy`) ARE caught by `Undecoded()`. Class-name typos are caught in the merge step by checking whether `classify.TrafficClass(className)` exists in `synth.ClassFreqConfigs`. The CONTEXT.md decision D-09 says unknown class names produce a warning (not error) to allow for Phase 7 user-defined classes — so this is by design.
|
||||
|
||||
**Warning signs:** Test for both: write a test with `[sounds.ICMP]` containing `frequncy = 440` (should error) AND a test with `[sounds.ICMP_TYPO]` containing `frequency = 440` (should warn, not error).
|
||||
|
||||
### Pitfall 2: Partial Override Accidentally Clears WaveformType
|
||||
|
||||
**What goes wrong:** User sets only `frequency = 300` for ICMP. After merge, ICMP's `WaveformType` is reset to `WaveformCustom` because the merge loop creates a new `FreqConfig{}` instead of starting from the default.
|
||||
|
||||
**Why it happens:** Copy-by-value from defaults is skipped, or merge starts from a zero-value struct.
|
||||
|
||||
**How to avoid:** Always start the merge from the DEFAULT `FreqConfig` for that class. The merge loop copies `defaults[class]` first, then overlays only non-nil pointer fields.
|
||||
|
||||
**Warning signs:** Test case: set only `frequency` for a class with `WaveformCustom` — verify waveform field is unchanged. Test case: set only `waveform` for a class — verify frequency is unchanged.
|
||||
|
||||
### Pitfall 3: Frequency Change Does Not Regenerate Harmonics for Preset Waveforms
|
||||
|
||||
**What goes wrong:** User sets `frequency = 300` for HTTPS (which has `WaveformCustom` by default, so this is fine). But if a user sets `frequency = 300` for a class that was previously configured with `WaveformSine` (via an earlier config entry), the harmonics may be stale from the old frequency.
|
||||
|
||||
**Why it happens:** `synth.WaveformPresetHarmonics` generates harmonics based on `baseHz`. If you update `BaseHz` without regenerating harmonics, the preset harmonics are anchored to the old frequency.
|
||||
|
||||
**How to avoid:** In the merge function: when updating `Frequency`, check if `WaveformType != WaveformCustom`. If true, regenerate `Harmonics` from the new frequency. The merge example above handles this correctly.
|
||||
|
||||
**Warning signs:** For the 14 built-in classes, all have `WaveformCustom` (hand-tuned harmonics), so this pitfall only bites if the user sets both `waveform` and `frequency` in two separate steps — or if a future phase pre-configures preset waveforms on built-ins.
|
||||
|
||||
### Pitfall 4: --config File-Not-Found vs Auto-Discovery Silence
|
||||
|
||||
**What goes wrong:** When `--config /path/to/missing.toml` is specified, the code returns the same "no config found, using defaults" behavior as auto-discovery silence.
|
||||
|
||||
**Why it happens:** `os.Stat` errors are treated uniformly regardless of how the path was obtained.
|
||||
|
||||
**How to avoid:** In the `Load` function, branch on whether `configPath` was explicitly provided: if it was, a `fs.ErrNotExist` is a user error (return error); if it came from auto-discovery, `fs.ErrNotExist` is normal (return `nil` error, use defaults).
|
||||
|
||||
**Warning signs:** CFG-03 acceptance criterion explicitly tests this: explicit path must error, absent auto-discovery must be silent.
|
||||
|
||||
### Pitfall 5: go.mod Tidy Drops TOML Dependency
|
||||
|
||||
**What goes wrong:** `go mod tidy` is run after adding BurntSushi/toml to go.mod but before any `.go` file in the module actually imports it. Tidy removes it.
|
||||
|
||||
**Why it happens:** `go mod tidy` removes unused dependencies.
|
||||
|
||||
**How to avoid:** Add the import in `config/config.go` before running `go mod tidy`.
|
||||
|
||||
## Code Examples
|
||||
|
||||
### Complete config.go Skeleton
|
||||
|
||||
```go
|
||||
// Source: BurntSushi/toml docs + project pattern
|
||||
// config/config.go
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
|
||||
"github.com/netsynth/netsynth/classify"
|
||||
"github.com/netsynth/netsynth/synth"
|
||||
)
|
||||
|
||||
type SoundOverride struct {
|
||||
Frequency *float64 `toml:"frequency"`
|
||||
Waveform *string `toml:"waveform"`
|
||||
}
|
||||
|
||||
type rawConfig struct {
|
||||
Sounds map[string]SoundOverride `toml:"sounds"`
|
||||
}
|
||||
|
||||
// Load is the single public entry point.
|
||||
// configPath: value of --config flag; empty = auto-discover.
|
||||
func Load(configPath string) (map[classify.TrafficClass]synth.FreqConfig, error) {
|
||||
path, explicit, err := resolvePath(configPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if path == "" {
|
||||
// No config found during auto-discovery — use defaults silently (CFG-02)
|
||||
return copyDefaults(), nil
|
||||
}
|
||||
|
||||
raw, err := parseFile(path)
|
||||
if err != nil {
|
||||
if explicit && errors.Is(err, fs.ErrNotExist) {
|
||||
return nil, fmt.Errorf("config file not found: %s", path)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := validate(raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return merge(copyDefaults(), raw.Sounds), nil
|
||||
}
|
||||
```
|
||||
|
||||
### Example TOML Config File
|
||||
|
||||
```toml
|
||||
# netsynth.toml — override ICMP and SSH sounds
|
||||
[sounds.ICMP]
|
||||
frequency = 80.0
|
||||
waveform = "square"
|
||||
|
||||
[sounds.SSH]
|
||||
frequency = 400.0
|
||||
# waveform not set — SSH keeps its default waveform
|
||||
```
|
||||
|
||||
### Test Pattern (table-driven)
|
||||
|
||||
```go
|
||||
// config/config_test.go
|
||||
|
||||
func TestLoadPartialOverride(t *testing.T) {
|
||||
// Write a temp TOML file with only frequency for ICMP
|
||||
tomlContent := `
|
||||
[sounds.ICMP]
|
||||
frequency = 100.0
|
||||
`
|
||||
f, _ := os.CreateTemp(t.TempDir(), "*.toml")
|
||||
f.WriteString(tomlContent)
|
||||
f.Close()
|
||||
|
||||
cfgs, err := Load(f.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
|
||||
// ICMP frequency overridden
|
||||
if cfgs[classify.ClassICMP].BaseHz != 100.0 {
|
||||
t.Errorf("ICMP BaseHz: got %v, want 100.0", cfgs[classify.ClassICMP].BaseHz)
|
||||
}
|
||||
// ICMP waveform unchanged (WaveformCustom = 0)
|
||||
if cfgs[classify.ClassICMP].WaveformType != synth.WaveformCustom {
|
||||
t.Errorf("ICMP WaveformType: got %v, want WaveformCustom", cfgs[classify.ClassICMP].WaveformType)
|
||||
}
|
||||
// DNS frequency unchanged
|
||||
if cfgs[classify.ClassDNS].BaseHz != synth.ClassFreqConfigs[classify.ClassDNS].BaseHz {
|
||||
t.Errorf("DNS BaseHz unexpectedly changed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadUnknownKey(t *testing.T) {
|
||||
tomlContent := `
|
||||
[sounds.ICMP]
|
||||
frequncy = 440
|
||||
`
|
||||
f, _ := os.CreateTemp(t.TempDir(), "*.toml")
|
||||
f.WriteString(tomlContent)
|
||||
f.Close()
|
||||
|
||||
_, err := Load(f.Name())
|
||||
if err == nil {
|
||||
t.Fatal("expected error for unknown key 'frequncy', got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "frequncy") {
|
||||
t.Errorf("error should name the bad key, got: %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## State of the Art
|
||||
|
||||
| Old Approach | Current Approach | When Changed | Impact |
|
||||
|--------------|------------------|--------------|--------|
|
||||
| `google/gopacket` | `gopacket/gopacket` v1.5.0 | 2022-2024 | N/A for this phase |
|
||||
| BurntSushi/toml v0.x | v1.6.0 (TOML 1.1 enabled by default) | December 2025 | TOML 1.1 compliance; API unchanged, same `Decode`/`DecodeFile` functions |
|
||||
| `go-audio/generator` | ARCHIVED (Feb 2026, read-only) | February 2026 | Do not use; project already avoids it |
|
||||
|
||||
**Current versions confirmed 2026-03-26:**
|
||||
- `BurntSushi/toml` v1.6.0 (December 18, 2025) — TOML 1.1 default, stable API
|
||||
- `pelletier/go-toml/v2` v2.3.0 (March 24, 2026) — alternative if structured error needed
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Typo suggestion for unknown keys (D-07 nice-to-have)**
|
||||
- What we know: BurntSushi returns `[]toml.Key` (structured), Levenshtein distance is ~15 lines of Go or `github.com/agnivade/levenshtein` (tiny, zero-dependency)
|
||||
- What's unclear: Is the complexity worth it for 2 valid field names per class block (`frequency`, `waveform`)?
|
||||
- Recommendation: Skip the external library. Implement inline: for each undecoded key, if it has edit distance ≤ 2 from any valid key name, append " (did you mean: X?)" to the error. The valid key set for field names is small and static: `["frequency", "waveform"]`. This is ~10 lines of Go.
|
||||
|
||||
2. **copyDefaults() — shallow vs deep copy of Harmonics slices**
|
||||
- What we know: `synth.FreqConfig.Harmonics` is a `[]HarmonicDef`. Go's `map[K]V` assignment copies struct values (including slice headers) but the underlying array is shared.
|
||||
- What's unclear: Does this matter if merge only replaces the whole slice (via `WaveformPresetHarmonics`) rather than appending to it?
|
||||
- Recommendation: Since the merge code assigns a freshly-generated `[]HarmonicDef` from `WaveformPresetHarmonics` (never mutates the original), shallow copy is safe. No deep copy needed. Document this in a comment for future maintainers.
|
||||
|
||||
## Environment Availability
|
||||
|
||||
Step 2.6: This phase introduces one new external dependency:
|
||||
|
||||
| Dependency | Required By | Available | Version | Fallback |
|
||||
|------------|------------|-----------|---------|----------|
|
||||
| `github.com/BurntSushi/toml` | config.Load() TOML parsing | ✓ (fetched via go get) | v1.6.0 | pelletier/go-toml v2.3.0 |
|
||||
| `os.UserConfigDir()` | Auto-discovery of `~/.config/netsynth/config.toml` | ✓ (Go stdlib) | Go 1.13+ | N/A — stdlib |
|
||||
| Go 1.24.1 toolchain | Module minimum | ✓ | 1.24.1 | N/A |
|
||||
| C compiler (CGo) | go-lame MP3 encoding (pre-existing) | Assumed ✓ (Phase 2+ already requires this) | — | N/A |
|
||||
|
||||
No missing dependencies with no fallback. BurntSushi/toml confirmed fetchable from pkg.go.dev.
|
||||
|
||||
## Validation Architecture
|
||||
|
||||
### Test Framework
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Framework | Go standard `testing` package |
|
||||
| Config file | None — `go test ./...` |
|
||||
| Quick run command | `go test ./config/...` |
|
||||
| Full suite command | `go test ./...` |
|
||||
|
||||
### Phase Requirements → Test Map
|
||||
|
||||
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|
||||
|--------|----------|-----------|-------------------|-------------|
|
||||
| CFG-01 | TOML overrides applied to correct class | unit | `go test ./config/... -run TestLoadOverride` | Wave 0 |
|
||||
| CFG-02 | No config file → silent, uses defaults | unit | `go test ./config/... -run TestLoadNoConfig` | Wave 0 |
|
||||
| CFG-03 | `--config` explicit path → error if missing | unit | `go test ./config/... -run TestLoadExplicitMissing` | Wave 0 |
|
||||
| CFG-04 | Partial override: unset fields unchanged | unit | `go test ./config/... -run TestLoadPartialOverride` | Wave 0 |
|
||||
| CFG-05 | Unknown key → error naming the key | unit | `go test ./config/... -run TestLoadUnknownKey` | Wave 0 |
|
||||
| CFG-03 | `--config` flag wired in Cobra | integration | `go test ./cmd/netsynth/... -run TestConfigFlag` | Wave 0 |
|
||||
|
||||
### Sampling Rate
|
||||
|
||||
- **Per task commit:** `go test ./config/... -count=1`
|
||||
- **Per wave merge:** `go test ./... -count=1`
|
||||
- **Phase gate:** Full suite green before `/gsd:verify-work`
|
||||
|
||||
### Wave 0 Gaps
|
||||
|
||||
- [ ] `config/config.go` — package does not exist yet; create in Wave 1
|
||||
- [ ] `config/config_test.go` — covers CFG-01 through CFG-05
|
||||
- [ ] `cmd/netsynth/main_test.go` — add `TestConfigFlag` covering CFG-03 CLI integration
|
||||
|
||||
*(Existing test infrastructure covers all other packages; only `config/` is new.)*
|
||||
|
||||
## Sources
|
||||
|
||||
### Primary (HIGH confidence)
|
||||
|
||||
- `pkg.go.dev/github.com/BurntSushi/toml` — v1.6.0 API: `DecodeFile`, `MetaData.Undecoded()`, `[]Key` type; verified 2026-03-26
|
||||
- Go stdlib `os.UserConfigDir()` — returns `$XDG_CONFIG_HOME` or `$HOME/.config` on Linux; Go 1.13+ feature
|
||||
- `pkg.go.dev/github.com/pelletier/go-toml/v2` — v2.3.0 `DisallowUnknownFields()` / `StrictMissingError` API; verified 2026-03-26
|
||||
|
||||
### Secondary (MEDIUM confidence)
|
||||
|
||||
- WebSearch: BurntSushi/toml Undecoded() approach verified against official GitHub source (`toml/decode.go`)
|
||||
- WebSearch: pelletier/go-toml v2 DisallowUnknownFields verified against official docs
|
||||
- WebSearch: `os.UserConfigDir` XDG compliance — confirmed returns `$XDG_CONFIG_HOME` or `$HOME/.config` on Linux per golang/go issue #29960
|
||||
|
||||
### Tertiary (LOW confidence)
|
||||
|
||||
- WebSearch: edit distance typo suggestion libraries (agnivade/levenshtein, go-edlib) — not deeply evaluated; recommendation is inline 10-line implementation to avoid dependency
|
||||
|
||||
## Metadata
|
||||
|
||||
**Confidence breakdown:**
|
||||
|
||||
- Standard stack: HIGH — versions confirmed via `go get` live fetch (v1.6.0 BurntSushi, v2.3.0 pelletier)
|
||||
- Architecture: HIGH — patterns derived from library documentation + existing codebase patterns
|
||||
- Pitfalls: HIGH — Undecoded() + map key limitation is a documented behavior; partial-override via pointer fields is an established Go idiom
|
||||
- TOML typo suggestion: LOW — nice-to-have from D-07; no deep investigation needed given small valid-key set
|
||||
|
||||
**Research date:** 2026-03-26
|
||||
**Valid until:** 2026-06-26 (BurntSushi/toml is stable; go-toml v2 moves faster but is not the chosen library)
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
---
|
||||
phase: 06
|
||||
slug: config-package-and-sound-overrides
|
||||
status: draft
|
||||
nyquist_compliant: false
|
||||
wave_0_complete: false
|
||||
created: 2026-03-26
|
||||
---
|
||||
|
||||
# Phase 06 — Validation Strategy
|
||||
|
||||
> Per-phase validation contract for feedback sampling during execution.
|
||||
|
||||
---
|
||||
|
||||
## Test Infrastructure
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Framework** | go test (stdlib) |
|
||||
| **Config file** | none — tests use in-memory TOML strings |
|
||||
| **Quick run command** | `go test ./config/... -count=1` |
|
||||
| **Full suite command** | `go test ./... -count=1` |
|
||||
| **Estimated runtime** | ~2 seconds |
|
||||
|
||||
---
|
||||
|
||||
## Sampling Rate
|
||||
|
||||
- **After every task commit:** Run `go test ./config/... -count=1`
|
||||
- **After every plan wave:** Run `go test ./... -count=1`
|
||||
- **Before `/gsd:verify-work`:** Full suite must be green
|
||||
- **Max feedback latency:** 2 seconds
|
||||
|
||||
---
|
||||
|
||||
## Per-Task Verification Map
|
||||
|
||||
| Task ID | Plan | Wave | Requirement | Test Type | Automated Command | File Exists | Status |
|
||||
|---------|------|------|-------------|-----------|-------------------|-------------|--------|
|
||||
| 06-01-01 | 01 | 1 | CFG-01, CFG-04 | unit | `go test ./config/... -run TestParse` | ❌ W0 | ⬜ pending |
|
||||
| 06-01-02 | 01 | 1 | CFG-02, CFG-03 | unit | `go test ./config/... -run TestDiscover` | ❌ W0 | ⬜ pending |
|
||||
| 06-01-03 | 01 | 1 | CFG-05 | unit | `go test ./config/... -run TestUnknown` | ❌ W0 | ⬜ pending |
|
||||
| 06-02-01 | 02 | 2 | CFG-01, CFG-04 | integration | `go test ./... -run TestRunSynthesis` | ❌ W0 | ⬜ pending |
|
||||
| 06-02-02 | 02 | 2 | CFG-03 | integration | `go test ./cmd/... -run TestConfig` | ❌ W0 | ⬜ pending |
|
||||
|
||||
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
|
||||
|
||||
---
|
||||
|
||||
## Wave 0 Requirements
|
||||
|
||||
- [ ] `config/config_test.go` — stubs for parse, discover, validate, merge tests
|
||||
- [ ] Existing `go test` infrastructure covers all phase requirements
|
||||
|
||||
*Existing test infrastructure (go test) covers all phase requirements. New test files created alongside implementation.*
|
||||
|
||||
---
|
||||
|
||||
## Manual-Only Verifications
|
||||
|
||||
| Behavior | Requirement | Why Manual | Test Instructions |
|
||||
|----------|-------------|------------|-------------------|
|
||||
| Auto-discover from `~/.config/netsynth/config.toml` | CFG-02 | Requires real home directory | Create config in `~/.config/netsynth/`, run netsynth, verify it loads |
|
||||
| Ctrl+C after config load | CFG-01 | End-to-end with capture | Load config, start capture, Ctrl+C, verify MP3 uses overridden frequency |
|
||||
|
||||
*Most behaviors have automated verification via unit tests with in-memory TOML.*
|
||||
|
||||
---
|
||||
|
||||
## Validation Sign-Off
|
||||
|
||||
- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
|
||||
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
|
||||
- [ ] Wave 0 covers all MISSING references
|
||||
- [ ] No watch-mode flags
|
||||
- [ ] Feedback latency < 2s
|
||||
- [ ] `nyquist_compliant: true` set in frontmatter
|
||||
|
||||
**Approval:** pending
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
---
|
||||
phase: 06-config-package-and-sound-overrides
|
||||
verified: 2026-03-26T20:15:00Z
|
||||
status: passed
|
||||
score: 10/10 must-haves verified
|
||||
re_verification: false
|
||||
---
|
||||
|
||||
# Phase 6: Config Package and Sound Overrides Verification Report
|
||||
|
||||
**Phase Goal:** Users can create a TOML config file to override frequency and waveform per traffic class, with auto-discovery, partial override semantics, and clear validation errors
|
||||
**Verified:** 2026-03-26T20:15:00Z
|
||||
**Status:** passed
|
||||
**Re-verification:** No — initial verification
|
||||
|
||||
## Goal Achievement
|
||||
|
||||
### Observable Truths
|
||||
|
||||
| # | Truth | Status | Evidence |
|
||||
|----|-------------------------------------------------------------------------------------|------------|--------------------------------------------------------------------------------------------|
|
||||
| 1 | Load with explicit path to valid TOML returns merged config map with overrides applied | ✓ VERIFIED | TestLoadPartialOverrideFrequency, TestLoadBothOverrides — PASS |
|
||||
| 2 | Load with no config file found returns default ClassFreqConfigs unchanged | ✓ VERIFIED | TestLoadNoConfig (t.Chdir to empty tmpdir) — PASS |
|
||||
| 3 | Load with unknown TOML key returns error naming the bad key | ✓ VERIFIED | TestLoadUnknownKey ("frequncy") — PASS; error contains the typo'd key name |
|
||||
| 4 | Load with partial override (only frequency set) leaves waveform unchanged | ✓ VERIFIED | TestLoadPartialOverrideFrequency — WaveformType remains WaveformCustom — PASS |
|
||||
| 5 | Load with partial override (only waveform set) leaves frequency unchanged | ✓ VERIFIED | TestLoadPartialOverrideWaveform — BaseHz remains 65.0 — PASS |
|
||||
| 6 | Load with unknown class name logs warning and does not error | ✓ VERIFIED | TestLoadUnknownClass (BOGUS class) — err == nil, 14 entries, warning to stderr — PASS |
|
||||
| 7 | User passes --config /path/to/file.toml and tool uses that file for sound overrides | ✓ VERIFIED | config.Load(configPath) called in run() at line 87; flows to RunSynthesis and NewBank |
|
||||
| 8 | User passes --config /nonexistent.toml and tool exits with clear error before capture | ✓ VERIFIED | Tested live: `go run ./cmd/netsynth --config /nonexistent/file.toml` exits 1 with "config file not found: /nonexistent/file.toml" |
|
||||
| 9 | User runs without --config and auto-discovery kicks in (or defaults used silently) | ✓ VERIFIED | discoverPath() checks ./netsynth.toml then XDG dir; silent default on no-find |
|
||||
| 10 | RunSynthesis uses the merged config map instead of hardcoded ClassFreqConfigs | ✓ VERIFIED | encode/mp3.go line 58: `synth.NewBank(1.0, freqCfgs)` — no reference to ClassFreqConfigs |
|
||||
|
||||
**Score:** 10/10 truths verified
|
||||
|
||||
### Required Artifacts
|
||||
|
||||
| Artifact | Expected | Status | Details |
|
||||
|-------------------------|---------------------------------------------------|------------|------------------------------------------------------------------------|
|
||||
| `config/config.go` | Load function, parse, validate, merge, discover | ✓ VERIFIED | 184 lines; exports Load, SoundOverride, rawConfig; all functions present |
|
||||
| `config/config_test.go` | Table-driven tests for CFG-01 through CFG-05 | ✓ VERIFIED | 181 lines; 9 test functions (TestLoad*); all 9 pass |
|
||||
| `cmd/netsynth/main.go` | --config flag, config.Load call, freqCfgs to RunSynthesis | ✓ VERIFIED | configPath var, flag registration, config.Load at line 87, two RunSynthesis call sites updated |
|
||||
| `encode/mp3.go` | RunSynthesis with freqCfgs parameter | ✓ VERIFIED | Signature: `func RunSynthesis(..., freqCfgs map[classify.TrafficClass]synth.FreqConfig) error` |
|
||||
| `encode/mp3_test.go` | Updated tests for new RunSynthesis signature | ✓ VERIFIED | Three call sites pass `synth.ClassFreqConfigs` as third arg |
|
||||
|
||||
### Key Link Verification
|
||||
|
||||
| From | To | Via | Status | Details |
|
||||
|---------------------------|---------------------------|--------------------------------------------------|------------|-----------------------------------------------------------|
|
||||
| `config/config.go` | `synth/config.go` | synth.FreqConfig, ClassFreqConfigs, WaveformPresetHarmonics | ✓ WIRED | grep confirmed all three at lines 46, 147-148, 172, 178 |
|
||||
| `config/config.go` | `classify/types.go` | classify.TrafficClass (AllClasses implied) | ✓ WIRED | Line 161: `classify.TrafficClass(className)` confirmed |
|
||||
| `config/config.go` | `github.com/BurntSushi/toml` | toml.DecodeFile, md.Undecoded() | ✓ WIRED | Lines 104 and 115 confirmed; dependency in go.mod |
|
||||
| `cmd/netsynth/main.go` | `config/config.go` | config.Load(configPath) | ✓ WIRED | Line 87: `freqCfgs, err := config.Load(configPath)` |
|
||||
| `cmd/netsynth/main.go` | `encode/mp3.go` | encode.RunSynthesis(snapshots, outputPath, freqCfgs) | ✓ WIRED | Lines 157 and 225 — both runLiveMode and runPcapMode |
|
||||
| `encode/mp3.go` | `synth/bank.go` | synth.NewBank(1.0, freqCfgs) using passed-in config | ✓ WIRED | Line 58: `synth.NewBank(1.0, freqCfgs)` — no hardcoding |
|
||||
|
||||
### Data-Flow Trace (Level 4)
|
||||
|
||||
| Artifact | Data Variable | Source | Produces Real Data | Status |
|
||||
|-----------------------|---------------|-------------------------------|--------------------|-------------|
|
||||
| `config/config.go` | result map | synth.ClassFreqConfigs + TOML overrides | Yes — copies from ClassFreqConfigs (14 entries), overlays TOML | ✓ FLOWING |
|
||||
| `encode/mp3.go` | freqCfgs | Injected from config.Load | Yes — passed in from caller, not hardcoded | ✓ FLOWING |
|
||||
| `cmd/netsynth/main.go`| freqCfgs | config.Load(configPath) return | Yes — real config.Load result, error-guarded | ✓ FLOWING |
|
||||
|
||||
### Behavioral Spot-Checks
|
||||
|
||||
| Behavior | Command | Result | Status |
|
||||
|----------------------------------------------------|---------------------------------------------------------------|-----------------------------------------------------|---------|
|
||||
| `--config` flag appears in CLI help | `go run ./cmd/netsynth --help` | `--config string Path to TOML config file (default: auto-discover)` | ✓ PASS |
|
||||
| Explicit --config missing file errors before capture | `go run ./cmd/netsynth --config /nonexistent/file.toml --read /dev/null` | exit 1, "config file not found: /nonexistent/file.toml" | ✓ PASS |
|
||||
| All 9 config package tests pass | `go test ./config/... -count=1 -v` | All 9 TestLoad* PASS | ✓ PASS |
|
||||
| Full test suite green | `go test ./... -count=1` | 7 packages all ok | ✓ PASS |
|
||||
| Binary builds and vets clean | `go build ./... && go vet ./...` | BUILD OK, VET OK | ✓ PASS |
|
||||
|
||||
### Requirements Coverage
|
||||
|
||||
| Requirement | Source Plan | Description | Status | Evidence |
|
||||
|-------------|-------------|------------------------------------------------------------------------|-------------|------------------------------------------------------------------|
|
||||
| CFG-01 | 06-01 | User can create a TOML config file that overrides default sound mappings | ✓ SATISFIED | config.Load + merge; TestLoadPartialOverrideFrequency PASS |
|
||||
| CFG-02 | 06-01 | Tool auto-discovers config from ./netsynth.toml or ~/.config/netsynth/config.toml (silent if absent) | ✓ SATISFIED | discoverPath(); TestLoadNoConfig PASS (t.Chdir to empty dir) |
|
||||
| CFG-03 | 06-02 | User can specify explicit config path via --config flag (error if missing) | ✓ SATISFIED | --config flag registered; config.Load returns "not found" error |
|
||||
| CFG-04 | 06-01 | User can override individual values without replicating entire default config | ✓ SATISFIED | Pointer fields (*float64, *string); TestLoadPartialOverrideWaveform PASS |
|
||||
| CFG-05 | 06-01 | Unknown keys in config file produce a clear error with the typo'd key name | ✓ SATISFIED | md.Undecoded() + keyPath extraction; TestLoadUnknownKey PASS |
|
||||
|
||||
All 5 requirement IDs from both PLAN frontmatter entries (CFG-01, CFG-02, CFG-04, CFG-05 from 06-01; CFG-03 from 06-02) are satisfied with evidence.
|
||||
|
||||
**Orphaned requirements check:** REQUIREMENTS.md traceability table maps CFG-01 through CFG-05 to Phase 6. All 5 are claimed and verified. No orphans.
|
||||
|
||||
### Anti-Patterns Found
|
||||
|
||||
| File | Line | Pattern | Severity | Impact |
|
||||
|--------------------|------|-------------------|----------|---------|
|
||||
| `go.mod` | 14 | BurntSushi/toml marked `// indirect` despite being a direct import in config/config.go | Info | None functional — `go mod tidy` corrects it; does not affect build or tests |
|
||||
|
||||
No placeholders, stub functions, hardcoded empty returns, or TODO markers found in any phase 6 modified files.
|
||||
|
||||
### Human Verification Required
|
||||
|
||||
No items require human verification. All functional behaviors were confirmed programmatically:
|
||||
- Config loading, merging, and validation verified via unit tests.
|
||||
- CLI flag confirmed in help output.
|
||||
- Error-before-capture behavior confirmed via live CLI invocation.
|
||||
|
||||
### Gaps Summary
|
||||
|
||||
No gaps. All 10 observable truths verified, all 5 artifacts substantive and wired, all 6 key links confirmed, all 5 requirements satisfied. The single info-level finding (BurntSushi/toml marked indirect in go.mod) is a trivial go module hygiene item — `go mod tidy` resolves it and it has no impact on correctness or functionality.
|
||||
|
||||
---
|
||||
|
||||
_Verified: 2026-03-26T20:15:00Z_
|
||||
_Verifier: Claude (gsd-verifier)_
|
||||
Reference in New Issue
Block a user