--- phase: 07-custom-rules-and-print-config plan: 02 type: execute wave: 2 depends_on: - "07-01" files_modified: - cmd/netsynth/main.go - cmd/netsynth/main_test.go - config/config.go - config/config_test.go autonomous: true requirements: - RULE-02 - CFG-06 must_haves: truths: - "User runs netsynth --print-config and sees full effective config as commented TOML on stdout without capture starting" - "User rules prepend before built-in rules so first-match-wins gives user priority" - "Print-config output shows source path when config file loaded" - "Print-config output annotates defaults vs overrides vs auto-assigned" - "Print-config works without -i flag" - "Print-config includes [[rules]] section when user rules are present" artifacts: - path: "cmd/netsynth/main.go" provides: "--print-config flag, runPrintConfig(), user rule prepend" contains: "print-config" - path: "config/config.go" provides: "PrintConfig function" contains: "func PrintConfig(" - path: "cmd/netsynth/main_test.go" provides: "Tests for --print-config flag" contains: "TestPrintConfigFlagRegistered" - path: "config/config_test.go" provides: "Tests for PrintConfig output" contains: "TestPrintConfigContainsAllClasses" key_links: - from: "cmd/netsynth/main.go" to: "config/config.go" via: "runPrintConfig calls config.Load then config.PrintConfig" pattern: "config\\.PrintConfig" - from: "cmd/netsynth/main.go" to: "classify/rules.go" via: "append(result.UserRules, classify.DefaultRules...)" pattern: "append.*UserRules.*DefaultRules" --- Wire the LoadResult into main.go (user rules prepend, --print-config flag), implement the PrintConfig output function, and add comprehensive tests for both. Purpose: Completes RULE-02 (user rules fire before built-ins at the CLI level) and CFG-06 (--print-config UX). This is the final plan for Phase 7 and the v1.1 milestone. Output: Updated main.go with --print-config and user rule prepending; PrintConfig function in config package; tests in both test files. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/07-custom-rules-and-print-config/07-CONTEXT.md @.planning/phases/07-custom-rules-and-print-config/07-RESEARCH.md @.planning/phases/07-custom-rules-and-print-config/07-01-SUMMARY.md @cmd/netsynth/main.go @cmd/netsynth/main_test.go @config/config.go @config/config_test.go @classify/rules.go @classify/types.go @synth/config.go From config/config.go (after Plan 01): ```go type LoadResult struct { FreqCfgs map[classify.TrafficClass]synth.FreqConfig UserRules []classify.Rule ConfigPath string } func Load(configPath string) (LoadResult, error) ``` From classify/rules.go: ```go var DefaultRules = []Rule{ ... } // 12 rules func NewClassifier(rules []Rule) *Classifier ``` From synth/config.go: ```go var ClassFreqConfigs = map[classify.TrafficClass]FreqConfig{ ... } // 14 entries type FreqConfig struct { BaseHz float64 Harmonics []HarmonicDef Pan float64 WaveformType WaveformType } ``` From classify/types.go: ```go func AllClasses() []TrafficClass // 14 built-in classes in display order ``` From cmd/netsynth/main.go (current run() flow): ```go // listIfaces check is first early-exit // then mutual exclusion check for --read and -i // then interface-required check // then BPF filter validation // then config.Load(configPath) // then output path resolution // then runLiveMode or runPcapMode ``` From cmd/netsynth/main_test.go (patterns): ```go func newTestCmd() *cobra.Command // creates fresh command with all flags // Tests use rootCmd.SetArgs, rootCmd.Execute(), check err // PersistentPreRunE wires test vars to package-level vars ``` Task 1: Wire LoadResult into main.go and add --print-config flag cmd/netsynth/main.go, cmd/netsynth/main_test.go cmd/netsynth/main.go, cmd/netsynth/main_test.go, config/config.go, classify/rules.go **main.go changes:** 1. Add package-level var for print-config flag: ```go var printConfig bool // add alongside existing configPath var ``` 2. Register --print-config flag in main() alongside existing flags: ```go rootCmd.Flags().BoolVar(&printConfig, "print-config", false, "Print effective config as commented TOML and exit") ``` 3. In run(), add --print-config check as the SECOND early-exit (after listIfaces, BEFORE the mutual exclusion check). Per Pitfall 2 from research, this must come before the interface-required validation so `netsynth --print-config` works without `-i`: ```go // --print-config mode (CFG-06, D-09/D-10) if printConfig { return runPrintConfig() } ``` 4. Add runPrintConfig function: ```go func runPrintConfig() error { result, err := config.Load(configPath) if err != nil { return err } output := config.PrintConfig(result) fmt.Print(output) return nil } ``` 5. Update ALL Load() call sites to use LoadResult (there is one in run()): ```go // Load config (CFG-01 through CFG-05, D-11: fail fast before capture) result, err := config.Load(configPath) if err != nil { return err } ``` 6. After config load, prepend user rules before creating classifier (per D-04, RULE-02). Update BOTH runLiveMode and runPcapMode. Change their signatures to accept LoadResult instead of bare map: ```go func runLiveMode(cmd *cobra.Command, result config.LoadResult) error { // ... // D-04: user rules prepend before built-ins; first-match-wins allRules := append(result.UserRules, classify.DefaultRules...) classifier := classify.NewClassifier(allRules) // ...use result.FreqCfgs where freqCfgs was used before... } ``` Do the same for runPcapMode. Update the call sites in run(): ```go if readPath != "" { return runPcapMode(cmd, result) } return runLiveMode(cmd, result) ``` 7. In both runLiveMode and runPcapMode, replace `freqCfgs` parameter usage with `result.FreqCfgs` in the encode.RunSynthesis call: ```go if err := encode.RunSynthesis(collectedSnapshots, outputPath, result.FreqCfgs); err != nil { ``` **main_test.go changes:** 8. Update newTestCmd() to include --print-config flag and --config flag: ```go var testPrintConfig bool var testConfigPath string // ...in flag registration: rootCmd.Flags().BoolVar(&testPrintConfig, "print-config", false, "Print effective config") rootCmd.Flags().StringVar(&testConfigPath, "config", "", "Path to TOML config file") // ...in PersistentPreRunE: printConfig = testPrintConfig configPath = testConfigPath ``` 9. Add TestPrintConfigFlagRegistered: ```go func TestPrintConfigFlagRegistered(t *testing.T) { rootCmd := newTestCmd() f := rootCmd.Flags().Lookup("print-config") if f == nil { t.Fatal("expected --print-config flag to be registered") } } ``` 10. Add TestPrintConfigNoInterface -- verifies --print-config works without -i: ```go func TestPrintConfigNoInterface(t *testing.T) { // Reset globals ifaceName = "" listIfaces = false printConfig = false configPath = "" // ... reset all globals t.Chdir(t.TempDir()) // no netsynth.toml in temp dir rootCmd := newTestCmd() rootCmd.SetArgs([]string{"--print-config"}) var outBuf, errBuf bytes.Buffer rootCmd.SetOut(&outBuf) rootCmd.SetErr(&errBuf) err := rootCmd.Execute() if err != nil { t.Fatalf("--print-config should not require -i, got error: %v", err) } } ``` 11. Add TestPrintConfigWithConfigFile -- verifies print-config loads and displays a user config: ```go func TestPrintConfigWithConfigFile(t *testing.T) { // Create temp TOML with an override dir := t.TempDir() tomlPath := filepath.Join(dir, "test.toml") os.WriteFile(tomlPath, []byte("[sounds.ICMP]\nfrequency = 100.0\n"), 0644) // Reset globals // ... rootCmd := newTestCmd() rootCmd.SetArgs([]string{"--print-config", "--config", tomlPath}) var outBuf, errBuf bytes.Buffer rootCmd.SetOut(&outBuf) rootCmd.SetErr(&errBuf) // Capture stdout by redirecting os.Stdout temporarily, OR // check that no error occurred (PrintConfig writes to os.Stdout via fmt.Print) err := rootCmd.Execute() if err != nil { t.Fatalf("--print-config with --config should succeed, got: %v", err) } } ``` go test ./cmd/netsynth/... -v -count=1 && go test ./config/... -count=1 - cmd/netsynth/main.go contains `var printConfig bool` - cmd/netsynth/main.go contains `"print-config"` flag registration - cmd/netsynth/main.go contains `if printConfig {` BEFORE the interface-required check - cmd/netsynth/main.go contains `func runPrintConfig() error` - cmd/netsynth/main.go contains `append(result.UserRules, classify.DefaultRules...)` - cmd/netsynth/main.go contains `func runLiveMode(cmd *cobra.Command, result config.LoadResult)` - cmd/netsynth/main.go contains `func runPcapMode(cmd *cobra.Command, result config.LoadResult)` - cmd/netsynth/main_test.go contains `TestPrintConfigFlagRegistered` - cmd/netsynth/main_test.go contains `TestPrintConfigNoInterface` - cmd/netsynth/main_test.go contains `"print-config"` in newTestCmd() - cmd/netsynth/main_test.go contains `"config"` flag in newTestCmd() - `go test ./cmd/netsynth/... -count=1` exits 0 - `go test ./config/... -count=1` exits 0 --print-config flag registered, checked before interface validation (no -i required). runPrintConfig calls config.Load then config.PrintConfig, prints to stdout, exits. User rules prepended in both runLiveMode and runPcapMode via append(result.UserRules, classify.DefaultRules...). All existing and new tests pass. Task 2: Implement PrintConfig output function with comment annotations config/config.go, config/config_test.go config/config.go, config/config_test.go, synth/config.go, classify/types.go, classify/rules.go Add the PrintConfig function to config/config.go and tests to config/config_test.go. **config/config.go additions:** 1. Add an `AutoClasses` field to LoadResult to track which classes were auto-assigned (per Open Question 1 from research): ```go type LoadResult struct { FreqCfgs map[classify.TrafficClass]synth.FreqConfig UserRules []classify.Rule ConfigPath string AutoClasses map[classify.TrafficClass]bool // classes with auto-assigned frequencies } ``` Update addAutoFreqEntries to populate this map. Also update Load() to initialize the map. 2. Add the PrintConfig function. Use manual string building with fmt.Fprintf to a strings.Builder (per D-09, D-10, D-11). The function signature: ```go func PrintConfig(result LoadResult) string ``` 3. Output format (per Pattern 5 from research): ``` # NetSynth effective configuration # Config source: # Generated: ``` Then, if result.UserRules is non-empty, emit the [[rules]] section: ``` # Classification rules (user-defined, prepended before built-in rules) [[rules]] port = 8080 protocol = "tcp" class = "MyApp" ``` For each user rule, emit a `[[rules]]` block. If DstPort == 0, omit the `port` line (per D-02 semantics). Then emit the [sounds] section. Iterate in a deterministic order: first AllClasses() (14 built-in classes in display order), then any user-defined classes sorted alphabetically. For each class: ``` # -- Hz () [sounds.] frequency = waveform = "" ``` Where annotation is: - `default` -- class is in synth.ClassFreqConfigs AND FreqCfgs entry matches the default BaseHz and WaveformType - `override` -- class is in synth.ClassFreqConfigs BUT FreqCfgs entry differs from default (user changed it) - `auto-assigned` -- class is in result.AutoClasses 4. Add waveformString helper to convert WaveformType back to string: ```go func waveformString(wt synth.WaveformType) string { switch wt { case synth.WaveformSine: return "sine" case synth.WaveformSquare: return "square" case synth.WaveformSawtooth: return "sawtooth" case synth.WaveformTriangle: return "triangle" default: return "custom" } } ``` 5. For deterministic ordering of user-defined classes (not in AllClasses()), collect them, sort by string value, and append after built-in classes. **config/config_test.go additions:** 6. TestPrintConfigContainsAllClasses: Create a LoadResult with defaults (no overrides, no user rules). Call PrintConfig. Assert output contains all 14 class names from classify.AllClasses(): "ICMP", "DNS", "HTTPS", "HTTP", "SSH", "SMTP", "NTP", "DHCP", "other-TCP", "other-UDP", "unknown-1", "unknown-2", "unknown-3", "unknown-4". 7. TestPrintConfigSourcePath: Create LoadResult with ConfigPath="/home/user/netsynth.toml". Assert output contains `# Config source: /home/user/netsynth.toml`. 8. TestPrintConfigNoSourcePath: Create LoadResult with ConfigPath="". Assert output contains `# Config source: none`. 9. TestPrintConfigContainsRules: Create LoadResult with UserRules containing one rule (Protocol="tcp", DstPort=8080, Class="MyApp"). Assert output contains `[[rules]]`, `port = 8080`, `protocol = "tcp"`, `class = "MyApp"`. 10. TestPrintConfigRuleNoPort: Create LoadResult with UserRules containing rule with DstPort=0. Assert output does NOT contain `port =` for that rule. 11. TestPrintConfigDefaultAnnotation: Create LoadResult with defaults. Assert output contains `(default)` annotation for ICMP entry. 12. TestPrintConfigOverrideAnnotation: Create LoadResult where ICMP has BaseHz=100.0 (differs from default 65.0). Assert output contains `(override)` for ICMP. 13. TestPrintConfigAutoAssignedAnnotation: Create LoadResult with AutoClasses map containing "GameServer"=true. Assert output contains `(auto-assigned)` for GameServer entry. go test ./config/... -v -count=1 -run "PrintConfig" && go test ./... -count=1 - config/config.go contains `func PrintConfig(result LoadResult) string` - config/config.go contains `func waveformString(wt synth.WaveformType) string` - config/config.go LoadResult struct contains `AutoClasses map[classify.TrafficClass]bool` - config/config.go PrintConfig output contains `# NetSynth effective configuration` - config/config.go PrintConfig output contains `# Config source:` - config/config_test.go contains `TestPrintConfigContainsAllClasses` - config/config_test.go contains `TestPrintConfigSourcePath` - config/config_test.go contains `TestPrintConfigContainsRules` - config/config_test.go contains `TestPrintConfigDefaultAnnotation` - config/config_test.go contains `TestPrintConfigOverrideAnnotation` - config/config_test.go contains `TestPrintConfigAutoAssignedAnnotation` - `go test ./... -count=1` exits 0 (full suite green) PrintConfig produces commented TOML output with: header (source path, date), [[rules]] section for user rules (port omitted when 0), [sounds.*] section for all classes in deterministic order. Each sound entry annotated as (default), (override), or (auto-assigned). Full test suite green including all existing tests. - `go test ./... -count=1` -- full suite green - `go vet ./...` -- no issues - `netsynth --print-config` outputs commented TOML to stdout (manual check) - `netsynth --print-config --config ` shows overrides annotated as such - --print-config flag works without -i, outputs to stdout, exits without capture - User rules prepended before DefaultRules in both live and pcap modes - PrintConfig output contains all 14 built-in classes plus any user-defined classes - Comment annotations correctly distinguish default / override / auto-assigned - Source path shown in header when config loaded - [[rules]] section present in output when user rules exist - Full go test suite passes After completion, create `.planning/phases/07-custom-rules-and-print-config/07-02-SUMMARY.md`