docs(06): create phase plan
This commit is contained in:
@@ -53,7 +53,11 @@ Plans:
|
|||||||
3. User passes `--config /path/to/custom.toml` and the tool uses that file; if the file does not exist, the tool exits with a clear error before capture begins
|
3. User passes `--config /path/to/custom.toml` and the tool uses that file; if the file does not exist, the tool exits with a clear error before capture begins
|
||||||
4. User types `frequncy = 440` in their config file and the tool exits at startup with an error naming `frequncy` as an unrecognized key
|
4. User types `frequncy = 440` in their config file and the tool exits at startup with an error naming `frequncy` as an unrecognized key
|
||||||
5. User sets waveform for one class in TOML and leaves all other classes at their defaults — the unspecified classes are unchanged
|
5. User sets waveform for one class in TOML and leaves all other classes at their defaults — the unspecified classes are unchanged
|
||||||
**Plans**: TBD
|
**Plans:** 2 plans
|
||||||
|
|
||||||
|
Plans:
|
||||||
|
- [ ] 06-01-PLAN.md — Config package: TOML load, validate, merge with TDD (config/config.go, config/config_test.go)
|
||||||
|
- [ ] 06-02-PLAN.md — CLI wiring: --config flag, RunSynthesis signature change, main.go integration
|
||||||
|
|
||||||
### Phase 7: Custom Rules and Print-Config
|
### Phase 7: Custom Rules and Print-Config
|
||||||
**Goal**: Users can define their own traffic classification rules in TOML, assign custom sounds to them, and inspect the full effective config before capture begins
|
**Goal**: Users can define their own traffic classification rules in TOML, assign custom sounds to them, and inspect the full effective config before capture begins
|
||||||
@@ -75,5 +79,5 @@ Plans:
|
|||||||
| 3. Pipeline Integration and MVP | v1.0 | 2/2 | Complete | 2026-03-26 |
|
| 3. Pipeline Integration and MVP | v1.0 | 2/2 | Complete | 2026-03-26 |
|
||||||
| 4. Power User Features | v1.0 | 2/2 | Complete | 2026-03-26 |
|
| 4. Power User Features | v1.0 | 2/2 | Complete | 2026-03-26 |
|
||||||
| 5. Waveform Types and Bank Decoupling | v1.1 | 2/2 | Complete | 2026-03-26 |
|
| 5. Waveform Types and Bank Decoupling | v1.1 | 2/2 | Complete | 2026-03-26 |
|
||||||
| 6. Config Package and Sound Overrides | v1.1 | 0/? | Not started | - |
|
| 6. Config Package and Sound Overrides | v1.1 | 0/2 | Planning complete | - |
|
||||||
| 7. Custom Rules and Print-Config | v1.1 | 0/? | Not started | - |
|
| 7. Custom Rules and Print-Config | v1.1 | 0/? | Not started | - |
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user