From 683470a7b185d5c1b7f0394d9f2eb461de5187a6 Mon Sep 17 00:00:00 2001 From: Markus Graf Date: Fri, 27 Mar 2026 16:25:14 +0100 Subject: [PATCH] docs(11): create phase plan --- .planning/ROADMAP.md | 7 +- .../11-01-PLAN.md | 224 ++++++++++ .../11-02-PLAN.md | 415 ++++++++++++++++++ 3 files changed, 644 insertions(+), 2 deletions(-) create mode 100644 .planning/phases/11-synthesis-and-config-layer/11-01-PLAN.md create mode 100644 .planning/phases/11-synthesis-and-config-layer/11-02-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index aca6c0a..6ca4674 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -91,7 +91,10 @@ Plans: 2. A pcap or live capture that triggers Mail traffic produces tones that are audibly in the same timbral family — same waveform type, similar frequency register — while still being distinguishable from each other 3. Users can define `[groups]` in their TOML config to reassign a protocol to a different sound family, and --print-config reflects the reassignment 4. `go test ./...` passes and a listening test on a representative pcap confirms family identity is perceptually clear -**Plans**: TBD +**Plans:** 2 plans +Plans: +- [ ] 11-01-PLAN.md — Add 21 ClassFreqConfigs entries, update AllClasses() to 35, fix all count tests +- [ ] 11-02-PLAN.md — Refactor PrintConfig for group headers, add [groups] TOML support ## Progress @@ -107,4 +110,4 @@ Plans: | 8. Test and Constant Cleanup | v1.2 | 1/1 | Complete | 2026-03-27 | | 9. Frequency Design and Group Architecture | v1.2 | 1/2 | In Progress| | | 10. Classification Layer | v1.2 | 2/2 | Complete | 2026-03-27 | -| 11. Synthesis and Config Layer | v1.2 | 0/? | Not started | - | +| 11. Synthesis and Config Layer | v1.2 | 0/2 | In Progress | - | diff --git a/.planning/phases/11-synthesis-and-config-layer/11-01-PLAN.md b/.planning/phases/11-synthesis-and-config-layer/11-01-PLAN.md new file mode 100644 index 0000000..553feee --- /dev/null +++ b/.planning/phases/11-synthesis-and-config-layer/11-01-PLAN.md @@ -0,0 +1,224 @@ +--- +phase: 11-synthesis-and-config-layer +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - synth/config.go + - classify/types.go + - synth/bank_test.go + - config/config_test.go + - classify/classifier_test.go +autonomous: true +requirements: + - GRP-02 + - GRP-03 + +must_haves: + truths: + - "ClassFreqConfigs has exactly 35 entries matching AllClasses()" + - "Every new class has correct Hz, waveform, pan, and group from frequency allocation table" + - "LDAP, Kerberos, Syslog appear in AllClasses() and have Infrastructure group with Triangle waveform" + - "go test ./synth/... ./classify/... ./config/... all pass" + artifacts: + - path: "synth/config.go" + provides: "21 new ClassFreqConfigs entries" + contains: "classify.ClassIMAP" + - path: "classify/types.go" + provides: "AllClasses() returns 35 entries including LDAP/Kerberos/Syslog" + contains: "ClassLDAP" + key_links: + - from: "synth/config.go" + to: "classify/types.go" + via: "ClassFreqConfigs references TrafficClass constants" + pattern: "classify\\.Class(IMAP|POP3|SMTPSub|RDP|Telnet|VNC|FTP|SMB|TFTP|MySQL|PostgreSQL|Redis|MongoDB|SIP|QUIC|MDNS|SSDP|SNMP|LDAP|Kerberos|Syslog)" +--- + + +Add all 21 missing ClassFreqConfigs entries and update AllClasses() to include LDAP/Kerberos/Syslog, then fix every hardcoded count assertion across synth, config, and classify test files. + +Purpose: This is the foundational data layer for Phase 11 -- all subsequent work (PrintConfig group headers, TOML [groups]) depends on all 35 classes having complete synthesis configs. +Output: synth/config.go with 35 ClassFreqConfigs entries, classify/types.go with 35-entry AllClasses(), all count-based tests green. + + + +@$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 +@synth/config.go +@classify/types.go +@synth/bank_test.go +@synth/config_test.go +@config/config_test.go +@classify/classifier_test.go + + + + +From classify/types.go: +```go +type TrafficClass string +// Constants: ClassIMAP, ClassPOP3, ClassSMTPSub, ClassFTP, ClassSMB, ClassTFTP, +// ClassRDP, ClassTelnet, ClassVNC, ClassMySQL, ClassPostgreSQL, ClassRedis, +// ClassMongoDB, ClassMDNS, ClassSSDP, ClassSNMP, ClassSIP, ClassQUIC, +// ClassLDAP, ClassKerberos, ClassSyslog +func AllClasses() []TrafficClass +``` + +From synth/config.go: +```go +type FreqConfig struct { + BaseHz float64 + Harmonics []HarmonicDef + Pan float64 + WaveformType WaveformType + Group string +} +var ClassFreqConfigs = map[classify.TrafficClass]FreqConfig{...} +func WaveformPresetHarmonics(wt WaveformType, baseHz float64, sampleRate int) []HarmonicDef +``` + + + + + + + Task 1: Add 21 ClassFreqConfigs entries and update AllClasses() + synth/config.go, classify/types.go + synth/config.go, classify/types.go + +**synth/config.go** -- Add 21 new entries to the ClassFreqConfigs map, after the existing entries and before the closing brace. Use WaveformPresetHarmonics() for all entries (per D-01). The exact values from the frequency allocation table (lines 74-110 of synth/config.go): + +``` +// --- Infrastructure additions (Triangle, 93-118 Hz) --- +classify.ClassMDNS: {BaseHz: 93.0, WaveformType: WaveformTriangle, Harmonics: WaveformPresetHarmonics(WaveformTriangle, 93.0, SampleRate), Pan: 0.3, Group: "Infrastructure"} +classify.ClassSSDP: {BaseHz: 105.0, WaveformType: WaveformTriangle, Harmonics: WaveformPresetHarmonics(WaveformTriangle, 105.0, SampleRate), Pan: -0.2, Group: "Infrastructure"} +classify.ClassSNMP: {BaseHz: 118.0, WaveformType: WaveformTriangle, Harmonics: WaveformPresetHarmonics(WaveformTriangle, 118.0, SampleRate), Pan: 0.2, Group: "Infrastructure"} +// --- Web addition (Sawtooth, 190 Hz) --- +classify.ClassQUIC: {BaseHz: 190.0, WaveformType: WaveformSawtooth, Harmonics: WaveformPresetHarmonics(WaveformSawtooth, 190.0, SampleRate), Pan: -0.2, Group: "Web"} +// --- Mail additions (Triangle, 241-305 Hz) --- +classify.ClassIMAP: {BaseHz: 241.0, WaveformType: WaveformTriangle, Harmonics: WaveformPresetHarmonics(WaveformTriangle, 241.0, SampleRate), Pan: 0.3, Group: "Mail"} +classify.ClassPOP3: {BaseHz: 271.0, WaveformType: WaveformTriangle, Harmonics: WaveformPresetHarmonics(WaveformTriangle, 271.0, SampleRate), Pan: 0.4, Group: "Mail"} +classify.ClassSMTPSub: {BaseHz: 305.0, WaveformType: WaveformTriangle, Harmonics: WaveformPresetHarmonics(WaveformTriangle, 305.0, SampleRate), Pan: 0.5, Group: "Mail"} +// --- Remote Access additions (Square, 385-485 Hz) --- +classify.ClassRDP: {BaseHz: 385.0, WaveformType: WaveformSquare, Harmonics: WaveformPresetHarmonics(WaveformSquare, 385.0, SampleRate), Pan: -0.6, Group: "Remote Access"} +classify.ClassTelnet: {BaseHz: 432.0, WaveformType: WaveformSquare, Harmonics: WaveformPresetHarmonics(WaveformSquare, 432.0, SampleRate), Pan: -0.5, Group: "Remote Access"} +classify.ClassVNC: {BaseHz: 485.0, WaveformType: WaveformSquare, Harmonics: WaveformPresetHarmonics(WaveformSquare, 485.0, SampleRate), Pan: -0.4, Group: "Remote Access"} +// --- File Transfer additions (Square, 545-687 Hz) --- +classify.ClassFTP: {BaseHz: 545.0, WaveformType: WaveformSquare, Harmonics: WaveformPresetHarmonics(WaveformSquare, 545.0, SampleRate), Pan: 0.5, Group: "File Transfer"} +classify.ClassSMB: {BaseHz: 612.0, WaveformType: WaveformSquare, Harmonics: WaveformPresetHarmonics(WaveformSquare, 612.0, SampleRate), Pan: 0.6, Group: "File Transfer"} +classify.ClassTFTP: {BaseHz: 687.0, WaveformType: WaveformSquare, Harmonics: WaveformPresetHarmonics(WaveformSquare, 687.0, SampleRate), Pan: 0.7, Group: "File Transfer"} +// --- Database additions (Sawtooth, 1543-2182 Hz) --- +classify.ClassMySQL: {BaseHz: 1543.0, WaveformType: WaveformSawtooth, Harmonics: WaveformPresetHarmonics(WaveformSawtooth, 1543.0, SampleRate), Pan: -0.4, Group: "Database"} +classify.ClassPostgreSQL: {BaseHz: 1732.0, WaveformType: WaveformSawtooth, Harmonics: WaveformPresetHarmonics(WaveformSawtooth, 1732.0, SampleRate), Pan: -0.2, Group: "Database"} +classify.ClassRedis: {BaseHz: 1944.0, WaveformType: WaveformSawtooth, Harmonics: WaveformPresetHarmonics(WaveformSawtooth, 1944.0, SampleRate), Pan: 0.2, Group: "Database"} +classify.ClassMongoDB: {BaseHz: 2182.0, WaveformType: WaveformSawtooth, Harmonics: WaveformPresetHarmonics(WaveformSawtooth, 2182.0, SampleRate), Pan: 0.4, Group: "Database"} +// --- VoIP (Sine, 2449 Hz) --- +classify.ClassSIP: {BaseHz: 2449.0, WaveformType: WaveformSine, Harmonics: WaveformPresetHarmonics(WaveformSine, 2449.0, SampleRate), Pan: 0.0, Group: "VoIP"} +// --- Infrastructure auto-assigned (Triangle, 2950-3250 Hz) per D-02 --- +classify.ClassLDAP: {BaseHz: 2950.0, WaveformType: WaveformTriangle, Harmonics: WaveformPresetHarmonics(WaveformTriangle, 2950.0, SampleRate), Pan: -0.2, Group: "Infrastructure"} +classify.ClassKerberos: {BaseHz: 3250.0, WaveformType: WaveformTriangle, Harmonics: WaveformPresetHarmonics(WaveformTriangle, 3250.0, SampleRate), Pan: 0.0, Group: "Infrastructure"} +classify.ClassSyslog: {BaseHz: 3050.0, WaveformType: WaveformTriangle, Harmonics: WaveformPresetHarmonics(WaveformTriangle, 3050.0, SampleRate), Pan: 0.2, Group: "Infrastructure"} +``` + +Place new entries in the map grouped by family with section comments matching the existing pattern (e.g., `// --- Infrastructure additions ...`). Insert them logically: +- Infrastructure additions (mDNS, SSDP, SNMP) after ClassDHCP and before ClassDNS (since 93/105/118 Hz come between DHCP=82 and DNS=133) +- Web addition (QUIC) after ClassHTTP +- Mail additions after ClassSMTP +- Remote Access additions after ClassSSH +- File Transfer after Unknown entries +- Database after Unknown entries +- VoIP after Database +- LDAP/Kerberos/Syslog at end (auto-assigned range) + +**classify/types.go** -- Per D-02 and D-03: +1. Add ClassLDAP, ClassKerberos, ClassSyslog to AllClasses() in the Infrastructure section, after ClassSNMP. +2. Remove the comment "Excludes ClassLDAP, ClassKerberos, and ClassSyslog" from the AllClasses() doc comment. +3. Update the doc comment to say "AllClasses returns all known traffic classes in display order." +4. The AllClasses() function should now return 35 entries total. + + + cd /home/dev/workspace/yoloyolo && go build ./synth/... ./classify/... + + +- `grep -c "classify\.Class" synth/config.go` shows at least 35 occurrences in ClassFreqConfigs +- `grep "ClassLDAP" classify/types.go` appears in AllClasses() return slice +- `grep "ClassKerberos" classify/types.go` appears in AllClasses() return slice +- `grep "ClassSyslog" classify/types.go` appears in AllClasses() return slice +- `go build ./synth/... ./classify/...` succeeds + + ClassFreqConfigs has 35 entries with correct Hz/waveform/pan/group values. AllClasses() returns 35 entries including LDAP/Kerberos/Syslog. Both packages compile. + + + + Task 2: Fix all hardcoded count assertions in tests + synth/bank_test.go, config/config_test.go, classify/classifier_test.go + synth/bank_test.go, config/config_test.go, classify/classifier_test.go + +Update all hardcoded count assertions to reflect the new 35-entry state. Per research Pitfall 1 and Pitfall 2: + +**synth/bank_test.go:** +- Line 10: Rename `TestNewBankHas14Layers` to `TestNewBankHasAllLayers` +- Line 12: Change `len(b.layers) != 14` to `len(b.layers) != len(classify.AllClasses())` +- Line 13: Change `want 14` to a dynamic message using `len(classify.AllClasses())` + +**config/config_test.go:** +- Line 120: Change `len(cfgs) != 14` to `len(cfgs) != len(classify.AllClasses())` in TestLoadNoConfig +- Line 149: Change `len(cfgs) != 14` to `len(cfgs) != len(classify.AllClasses())` in TestLoadUnknownClass +- Line 180: Change `len(cfgs) != 14` to `len(cfgs) != len(classify.AllClasses())` in TestLoadAllDefaultsPresent +- Line 617: Change `len(result.FreqCfgs) != 14` to `len(result.FreqCfgs) != len(classify.AllClasses())` in TestLoadNoConfigReturnsLoadResult +- Lines 407-416 in TestPrintConfigContainsAllClasses: Replace the hardcoded `classNames` slice with a loop over `classify.AllClasses()`. Change to: + ```go + for _, cls := range classify.AllClasses() { + if !strings.Contains(output, string(cls)) { + t.Errorf("PrintConfig output missing class %q", cls) + } + } + ``` + +**classify/classifier_test.go:** +- Line 459 (approximately): Change `want 32` to `want 35` in TestAllClassesCount. Update the assertion value from 32 to 35. + +Use `len(classify.AllClasses())` for dynamic counts wherever possible (synth and config tests). For classifier_test.go, use the literal 35 since the test is specifically verifying the count is a known value per D-03. + + + cd /home/dev/workspace/yoloyolo && go test ./synth/... ./classify/... ./config/... + + +- `go test ./synth/...` passes (0 failures) +- `go test ./classify/...` passes (0 failures) +- `go test ./config/...` passes (0 failures) +- `grep "14" synth/bank_test.go` returns no lines with hardcoded layer counts +- `grep 'want 14' config/config_test.go` returns no matches +- `grep 'want 32' classify/classifier_test.go` returns no matches + + All test suites pass with 35 classes. No hardcoded counts of 14 or 32 remain in test assertions. TestNewBankHas14Layers renamed to TestNewBankHasAllLayers. + + + + + +```bash +cd /home/dev/workspace/yoloyolo && go test ./... +``` +All tests pass. ClassFreqConfigs has 35 entries matching AllClasses(). + + + +- `go test ./...` passes fully +- ClassFreqConfigs map has exactly 35 entries +- AllClasses() returns exactly 35 entries +- No hardcoded counts of 14 or 32 remain in test files +- Every new entry uses WaveformPresetHarmonics() (not hand-tuned harmonics) +- LDAP/Kerberos/Syslog have Group="Infrastructure" and WaveformTriangle + + + +After completion, create `.planning/phases/11-synthesis-and-config-layer/11-01-SUMMARY.md` + diff --git a/.planning/phases/11-synthesis-and-config-layer/11-02-PLAN.md b/.planning/phases/11-synthesis-and-config-layer/11-02-PLAN.md new file mode 100644 index 0000000..1831e6a --- /dev/null +++ b/.planning/phases/11-synthesis-and-config-layer/11-02-PLAN.md @@ -0,0 +1,415 @@ +--- +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` +