Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
445 lines
16 KiB
Markdown
445 lines
16 KiB
Markdown
---
|
|
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"
|
|
---
|
|
|
|
<objective>
|
|
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.
|
|
</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/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
|
|
|
|
<interfaces>
|
|
<!-- Post-Plan-01 interfaces the executor needs -->
|
|
|
|
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
|
|
```
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Wire LoadResult into main.go and add --print-config flag</name>
|
|
<files>cmd/netsynth/main.go, cmd/netsynth/main_test.go</files>
|
|
<read_first>cmd/netsynth/main.go, cmd/netsynth/main_test.go, config/config.go, classify/rules.go</read_first>
|
|
<action>
|
|
**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)
|
|
}
|
|
}
|
|
```
|
|
</action>
|
|
<verify>
|
|
<automated>go test ./cmd/netsynth/... -v -count=1 && go test ./config/... -count=1</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- 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
|
|
</acceptance_criteria>
|
|
<done>
|
|
--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.
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Implement PrintConfig output function with comment annotations</name>
|
|
<files>config/config.go, config/config_test.go</files>
|
|
<read_first>config/config.go, config/config_test.go, synth/config.go, classify/types.go, classify/rules.go</read_first>
|
|
<action>
|
|
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: <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
|
|
|
|
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.
|
|
</action>
|
|
<verify>
|
|
<automated>go test ./config/... -v -count=1 -run "PrintConfig" && go test ./... -count=1</automated>
|
|
</verify>
|
|
<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>
|
|
<done>
|
|
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.
|
|
</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `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 <file>` shows overrides annotated as such
|
|
</verification>
|
|
|
|
<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>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/07-custom-rules-and-print-config/07-02-SUMMARY.md`
|
|
</output>
|