--- phase: 11-synthesis-and-config-layer plan: 02 type: execute wave: 2 depends_on: ["11-01"] files_modified: - config/config.go - config/config_test.go autonomous: true requirements: - GRP-02 - GRP-03 must_haves: truths: - "PrintConfig output groups classes by family with section header comments" - "Groups appear in canonical order: Infrastructure, Web, Mail, Remote Access, File Transfer, Database, Discovery, VoIP, Unknown" - "Within each group, classes are sorted by ascending BaseHz" - "User-defined classes appear under a User-defined section header after all built-in groups" - "Users can define [groups] in TOML to reassign a class to a different group" - "Unknown class names in [groups] produce a warning, not an error" - "Group reassignment only affects PrintConfig grouping, not frequency or waveform" artifacts: - path: "config/config.go" provides: "Group-ordered PrintConfig, [groups] TOML support, applyGroupOverrides function" exports: ["PrintConfig", "Load", "LoadResult"] - path: "config/config_test.go" provides: "Tests for group headers, group reassignment, unknown class warning" contains: "TestPrintConfigGroupHeaders" key_links: - from: "config/config.go" to: "synth/config.go" via: "PrintConfig reads FreqConfig.Group field" pattern: "cfg\\.Group" - from: "config/config.go" to: "classify/types.go" via: "PrintConfig iterates AllClasses() and groups by Group field" pattern: "classify\\.AllClasses" --- Refactor PrintConfig to group classes by their Group field with section headers (GRP-02), and add [groups] TOML config support for reassigning protocols to different sound families (GRP-03). Purpose: This is the user-facing output change that makes --print-config show organized, family-coherent class listings, and gives users the ability to rearrange groupings via TOML config. Output: PrintConfig emits group headers in canonical order; [groups] TOML table parsed and applied; tests cover group headers, reassignment, and unknown class warnings. @$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/11-synthesis-and-config-layer/11-01-SUMMARY.md @config/config.go @config/config_test.go From synth/config.go (after Plan 01): ```go type FreqConfig struct { BaseHz float64 Harmonics []HarmonicDef Pan float64 WaveformType WaveformType Group string // "Infrastructure", "Web", "Mail", "Remote Access", "File Transfer", "Database", "Discovery", "VoIP", "Unknown" } var ClassFreqConfigs = map[classify.TrafficClass]FreqConfig{...} // 35 entries ``` From classify/types.go (after Plan 01): ```go func AllClasses() []TrafficClass // returns 35 entries including LDAP/Kerberos/Syslog ``` From config/config.go (current): ```go type rawConfig struct { Sounds map[string]SoundOverride `toml:"sounds"` Rules []RawRule `toml:"rules"` } type LoadResult struct { FreqCfgs map[classify.TrafficClass]synth.FreqConfig UserRules []classify.Rule ConfigPath string AutoClasses map[classify.TrafficClass]bool } func Load(configPath string) (LoadResult, error) func PrintConfig(result LoadResult) string ``` Task 1: Add [groups] TOML support and refactor PrintConfig for group headers config/config.go config/config.go Three changes to config/config.go: **1. Add Groups field to rawConfig struct (per D-07):** ```go type rawConfig struct { Sounds map[string]SoundOverride `toml:"sounds"` Rules []RawRule `toml:"rules"` Groups map[string]string `toml:"groups"` } ``` **2. Add applyGroupOverrides function and wire into Load() (per D-07/D-08/D-09):** Add a new function `applyGroupOverrides`: ```go // applyGroupOverrides overlays [groups] reassignments onto freqCfgs.Group in-place. // Unknown class names produce a warning to stderr (D-09). // Unknown group names are silently accepted -- users can invent custom groups (D-09). func applyGroupOverrides(cfgs map[classify.TrafficClass]synth.FreqConfig, groups map[string]string) { for className, groupName := range groups { cls := classify.TrafficClass(className) cfg, known := cfgs[cls] if !known { fmt.Fprintf(os.Stderr, "Warning: config: [groups]: unknown class %q (ignored)\n", className) continue } cfg.Group = groupName cfgs[cls] = cfg } } ``` In Load(), after the `merge(freqCfgs, raw.Sounds)` call (around line 101), add: ```go applyGroupOverrides(freqCfgs, raw.Groups) ``` This goes AFTER merge so that group reassignment is the last transformation before returning. The call should be present in both code paths (with config file). The no-config path (line 77) does not need it since there is no raw.Groups to apply. **3. Refactor PrintConfig for group-ordered output (per D-04/D-05/D-06):** Replace the current flat `AllClasses()` iteration (lines 298-321) and the user-defined section (lines 323-333) with group-ordered output. Add a package-level variable for canonical group order: ```go // groupOrder defines the canonical display order for --print-config section headers (D-04). var groupOrder = []string{ "Infrastructure", "Web", "Mail", "Remote Access", "File Transfer", "Database", "Discovery", "VoIP", "Unknown", } ``` Replace the built-in and user-defined emission blocks with: ```go // Build group -> []TrafficClass index from AllClasses() builtinByGroup := map[string][]classify.TrafficClass{} builtinSet := map[classify.TrafficClass]bool{} for _, cls := range classify.AllClasses() { builtinSet[cls] = true cfg := result.FreqCfgs[cls] grp := cfg.Group builtinByGroup[grp] = append(builtinByGroup[grp], cls) } // Sort each group by ascending BaseHz (D-05) using result.FreqCfgs (effective Hz, not defaults) for grp := range builtinByGroup { classes := builtinByGroup[grp] sort.Slice(classes, func(i, j int) bool { return result.FreqCfgs[classes[i]].BaseHz < result.FreqCfgs[classes[j]].BaseHz }) } // Emit built-in classes grouped with headers (D-04) for _, grp := range groupOrder { classes, ok := builtinByGroup[grp] if !ok || len(classes) == 0 { continue } fmt.Fprintf(&sb, "# %s\n\n", grp) for _, cls := range classes { cfg := result.FreqCfgs[cls] annotation := classAnnotation(cls, cfg, result.AutoClasses) fmt.Fprintf(&sb, "# %s -- %.1f Hz (%s)\n", string(cls), cfg.BaseHz, annotation) fmt.Fprintf(&sb, "[sounds.%s]\n", string(cls)) fmt.Fprintf(&sb, "frequency = %.1f\n", cfg.BaseHz) fmt.Fprintf(&sb, "waveform = %q\n", waveformString(cfg.WaveformType)) fmt.Fprintf(&sb, "\n") } } // Check for custom groups (from [groups] reassignment) that are not in groupOrder // These classes were already emitted under their reassigned group if the group is canonical. // For non-canonical group names (user-invented), collect and emit separately. customGroups := map[string][]classify.TrafficClass{} for _, cls := range classify.AllClasses() { cfg := result.FreqCfgs[cls] grp := cfg.Group isCanonical := false for _, cg := range groupOrder { if grp == cg { isCanonical = true break } } if !isCanonical { customGroups[grp] = append(customGroups[grp], cls) } } // Sort and emit custom group sections var customGroupNames []string for grp := range customGroups { customGroupNames = append(customGroupNames, grp) } sort.Strings(customGroupNames) for _, grp := range customGroupNames { classes := customGroups[grp] sort.Slice(classes, func(i, j int) bool { return result.FreqCfgs[classes[i]].BaseHz < result.FreqCfgs[classes[j]].BaseHz }) fmt.Fprintf(&sb, "# %s\n\n", grp) for _, cls := range classes { cfg := result.FreqCfgs[cls] annotation := classAnnotation(cls, cfg, result.AutoClasses) fmt.Fprintf(&sb, "# %s -- %.1f Hz (%s)\n", string(cls), cfg.BaseHz, annotation) fmt.Fprintf(&sb, "[sounds.%s]\n", string(cls)) fmt.Fprintf(&sb, "frequency = %.1f\n", cfg.BaseHz) fmt.Fprintf(&sb, "waveform = %q\n", waveformString(cfg.WaveformType)) fmt.Fprintf(&sb, "\n") } } // Emit user-defined classes (in FreqCfgs but not in AllClasses) under "# User-defined" (D-06) var userClasses []string for cls := range result.FreqCfgs { if !builtinSet[cls] { userClasses = append(userClasses, string(cls)) } } sort.Strings(userClasses) if len(userClasses) > 0 { fmt.Fprintf(&sb, "# User-defined\n\n") for _, clsStr := range userClasses { cls := classify.TrafficClass(clsStr) cfg := result.FreqCfgs[cls] annotation := classAnnotation(cls, cfg, result.AutoClasses) fmt.Fprintf(&sb, "# %s -- %.1f Hz (%s)\n", clsStr, cfg.BaseHz, annotation) fmt.Fprintf(&sb, "[sounds.%s]\n", clsStr) fmt.Fprintf(&sb, "frequency = %.1f\n", cfg.BaseHz) fmt.Fprintf(&sb, "waveform = %q\n", waveformString(cfg.WaveformType)) fmt.Fprintf(&sb, "\n") } } ``` Remove the old `builtinSet` declaration (line 298) since it is now declared in the new block. Remove the old `userClasses` collection and sort (lines 304-310). Remove the old built-in emission loop (lines 313-321) and user-defined emission loop (lines 323-333). Do NOT modify the header section (lines 273-281) or rules section (lines 283-295) -- those stay unchanged. Important: the `sort` package is already imported. The `builtinSet` map is now declared inside the new block, so remove the old one. cd /home/dev/workspace/yoloyolo && go build ./config/... - `grep "groupOrder" config/config.go` returns the canonical group order slice - `grep "applyGroupOverrides" config/config.go` returns the function definition - `grep 'Groups map\[string\]string' config/config.go` shows the new rawConfig field - `grep "# User-defined" config/config.go` shows the user-defined section header - `go build ./config/...` succeeds PrintConfig emits group-ordered output with section headers. rawConfig has Groups field. applyGroupOverrides function exists and is called in Load(). Compiles successfully. Task 2: Add tests for group headers, group reassignment, and unknown class warning config/config_test.go config/config_test.go, config/config.go Add 4 new test functions to config/config_test.go: **TestPrintConfigGroupHeaders** -- Verifies GRP-02 group header output: ```go func TestPrintConfigGroupHeaders(t *testing.T) { t.Chdir(t.TempDir()) result, err := config.Load("") if err != nil { t.Fatalf("Load: %v", err) } output := config.PrintConfig(result) // Verify all populated group headers appear expectedGroups := []string{"# Infrastructure", "# Web", "# Mail", "# Remote Access", "# File Transfer", "# Database", "# VoIP", "# Unknown"} for _, header := range expectedGroups { if !strings.Contains(output, header+"\n") { t.Errorf("PrintConfig output missing group header %q", header) } } // Verify canonical order: Infrastructure before Web before Mail etc. infraIdx := strings.Index(output, "# Infrastructure\n") webIdx := strings.Index(output, "# Web\n") mailIdx := strings.Index(output, "# Mail\n") remoteIdx := strings.Index(output, "# Remote Access\n") ftIdx := strings.Index(output, "# File Transfer\n") dbIdx := strings.Index(output, "# Database\n") voipIdx := strings.Index(output, "# VoIP\n") unknownIdx := strings.Index(output, "# Unknown\n") if infraIdx >= webIdx || webIdx >= mailIdx || mailIdx >= remoteIdx || remoteIdx >= ftIdx || ftIdx >= dbIdx || dbIdx >= voipIdx || voipIdx >= unknownIdx { t.Errorf("Group headers not in canonical order: infra=%d web=%d mail=%d remote=%d ft=%d db=%d voip=%d unknown=%d", infraIdx, webIdx, mailIdx, remoteIdx, ftIdx, dbIdx, voipIdx, unknownIdx) } } ``` **TestLoadGroupOverride** -- Verifies GRP-03 basic reassignment: ```go func TestLoadGroupOverride(t *testing.T) { path := writeTOML(t, "[groups]\nIMAP = \"Web\"\n") result, err := config.Load(path) if err != nil { t.Fatalf("Load: %v", err) } cfg := result.FreqCfgs[classify.ClassIMAP] if cfg.Group != "Web" { t.Errorf("IMAP Group: got %q, want %q", cfg.Group, "Web") } // Frequency and waveform unchanged (D-08) defaultCfg := synth.ClassFreqConfigs[classify.ClassIMAP] if cfg.BaseHz != defaultCfg.BaseHz { t.Errorf("IMAP BaseHz changed: got %v, want %v (should be unchanged by group reassignment)", cfg.BaseHz, defaultCfg.BaseHz) } } ``` **TestLoadGroupUnknownClass** -- Verifies D-09 warning for unknown class: ```go func TestLoadGroupUnknownClass(t *testing.T) { path := writeTOML(t, "[groups]\nBOGUS = \"Web\"\n") result, err := config.Load(path) if err != nil { t.Fatalf("Load should not error on unknown [groups] class: %v", err) } // Should still have all default classes if len(result.FreqCfgs) != len(classify.AllClasses()) { t.Errorf("FreqCfgs len: got %d, want %d", len(result.FreqCfgs), len(classify.AllClasses())) } } ``` **TestPrintConfigGroupReassignment** -- Verifies PrintConfig reflects reassignment: ```go func TestPrintConfigGroupReassignment(t *testing.T) { path := writeTOML(t, "[groups]\nIMAP = \"Web\"\n") result, err := config.Load(path) if err != nil { t.Fatalf("Load: %v", err) } output := config.PrintConfig(result) // Find the "# Web" section and check IMAP appears after it webIdx := strings.Index(output, "# Web\n") mailIdx := strings.Index(output, "# Mail\n") imapIdx := strings.Index(output, "[sounds.IMAP]") if imapIdx < webIdx || imapIdx > mailIdx { t.Errorf("IMAP (reassigned to Web) should appear between Web and Mail headers; web=%d imap=%d mail=%d", webIdx, imapIdx, mailIdx) } } ``` Also update `TestPrintConfigContainsAllClasses` if it was not already updated in Plan 01 -- it should use `classify.AllClasses()` loop instead of hardcoded class names. (Plan 01 should have done this, but verify and fix if needed.) cd /home/dev/workspace/yoloyolo && go test ./config/... -v -run "TestPrintConfigGroup|TestLoadGroup" - `go test ./config/... -run TestPrintConfigGroupHeaders` passes - `go test ./config/... -run TestLoadGroupOverride` passes - `go test ./config/... -run TestLoadGroupUnknownClass` passes - `go test ./config/... -run TestPrintConfigGroupReassignment` passes - `go test ./config/...` all tests pass (no regressions) Four new tests cover GRP-02 (group headers in canonical order) and GRP-03 (group reassignment, unknown class warning, PrintConfig reflects reassignment). Full config test suite passes. ```bash cd /home/dev/workspace/yoloyolo && go test ./... ``` All tests pass. PrintConfig shows group headers. [groups] TOML works for reassignment. Manual verification: `go run . --print-config` shows classes organized by group with `# Infrastructure`, `# Web`, `# Mail`, `# Remote Access`, `# File Transfer`, `# Database`, `# VoIP`, `# Unknown` section headers. - `go test ./...` passes fully - PrintConfig output contains group section headers in canonical order - [groups] TOML table reassigns a class's group in PrintConfig output - Unknown class names in [groups] produce stderr warning, not error - Group reassignment does not change frequency or waveform (D-08) - User-defined classes appear under "# User-defined" section After completion, create `.planning/phases/11-synthesis-and-config-layer/11-02-SUMMARY.md`