Files
yoloyolo/.planning/milestones/v1.1-phases/07-custom-rules-and-print-config/07-02-PLAN.md
T
2026-03-26 22:06:36 +01:00

16 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
07-custom-rules-and-print-config 02 execute 2
07-01
cmd/netsynth/main.go
cmd/netsynth/main_test.go
config/config.go
config/config_test.go
true
RULE-02
CFG-06
truths artifacts key_links
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
path provides contains
cmd/netsynth/main.go --print-config flag, runPrintConfig(), user rule prepend print-config
path provides contains
config/config.go PrintConfig function func PrintConfig(
path provides contains
cmd/netsynth/main_test.go Tests for --print-config flag TestPrintConfigFlagRegistered
path provides contains
config/config_test.go Tests for PrintConfig output TestPrintConfigContainsAllClasses
from to via pattern
cmd/netsynth/main.go config/config.go runPrintConfig calls config.Load then config.PrintConfig config.PrintConfig
from to via pattern
cmd/netsynth/main.go classify/rules.go append(result.UserRules, classify.DefaultRules...) 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.

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

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/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):

type LoadResult struct {
    FreqCfgs   map[classify.TrafficClass]synth.FreqConfig
    UserRules  []classify.Rule
    ConfigPath string
}

func Load(configPath string) (LoadResult, error)

From classify/rules.go:

var DefaultRules = []Rule{ ... } // 12 rules
func NewClassifier(rules []Rule) *Classifier

From synth/config.go:

var ClassFreqConfigs = map[classify.TrafficClass]FreqConfig{ ... } // 14 entries
type FreqConfig struct {
    BaseHz       float64
    Harmonics    []HarmonicDef
    Pan          float64
    WaveformType WaveformType
}

From classify/types.go:

func AllClasses() []TrafficClass // 14 built-in classes in display order

From cmd/netsynth/main.go (current run() flow):

// 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):

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:
var printConfig bool // add alongside existing configPath var
  1. Register --print-config flag in main() alongside existing flags:
rootCmd.Flags().BoolVar(&printConfig, "print-config", false, "Print effective config as commented TOML and exit")
  1. 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:
// --print-config mode (CFG-06, D-09/D-10)
if printConfig {
    return runPrintConfig()
}
  1. Add runPrintConfig function:
func runPrintConfig() error {
    result, err := config.Load(configPath)
    if err != nil {
        return err
    }
    output := config.PrintConfig(result)
    fmt.Print(output)
    return nil
}
  1. Update ALL Load() call sites to use LoadResult (there is one in run()):
// Load config (CFG-01 through CFG-05, D-11: fail fast before capture)
result, err := config.Load(configPath)
if err != nil {
    return err
}
  1. 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:
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():

if readPath != "" {
    return runPcapMode(cmd, result)
}
return runLiveMode(cmd, result)
  1. In both runLiveMode and runPcapMode, replace freqCfgs parameter usage with result.FreqCfgs in the encode.RunSynthesis call:
if err := encode.RunSynthesis(collectedSnapshots, outputPath, result.FreqCfgs); err != nil {

main_test.go changes:

  1. Update newTestCmd() to include --print-config flag and --config flag:
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
  1. Add TestPrintConfigFlagRegistered:
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")
    }
}
  1. Add TestPrintConfigNoInterface -- verifies --print-config works without -i:
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)
    }
}
  1. Add TestPrintConfigWithConfigFile -- verifies print-config loads and displays a user config:
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):
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.

  1. 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:
func PrintConfig(result LoadResult) string
  1. Output format (per Pattern 5 from research):
# NetSynth effective configuration
# Config source: <path or "none (using defaults)">
# Generated: <date>

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:

  # <ClassName> -- <BaseHz> Hz (<annotation>)
  [sounds.<ClassName>]
  frequency = <BaseHz>
  waveform = "<waveform_string>"

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
  1. Add waveformString helper to convert WaveformType back to string:
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"
    }
}
  1. 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:

  1. 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".

  2. TestPrintConfigSourcePath: Create LoadResult with ConfigPath="/home/user/netsynth.toml". Assert output contains # Config source: /home/user/netsynth.toml.

  3. TestPrintConfigNoSourcePath: Create LoadResult with ConfigPath="". Assert output contains # Config source: none.

  4. TestPrintConfigContainsRules: Create LoadResult with UserRules containing one rule (Protocol="tcp", DstPort=8080, Class="MyApp"). Assert output contains [[rules]], port = 8080, protocol = "tcp", class = "MyApp".

  5. TestPrintConfigRuleNoPort: Create LoadResult with UserRules containing rule with DstPort=0. Assert output does NOT contain port = for that rule.

  6. TestPrintConfigDefaultAnnotation: Create LoadResult with defaults. Assert output contains (default) annotation for ICMP entry.

  7. TestPrintConfigOverrideAnnotation: Create LoadResult where ICMP has BaseHz=100.0 (differs from default 65.0). Assert output contains (override) for ICMP.

  8. 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 <acceptance_criteria>

    • 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) </acceptance_criteria> 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

<success_criteria>

  • --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 </success_criteria>
After completion, create `.planning/phases/07-custom-rules-and-print-config/07-02-SUMMARY.md`