docs(11): create phase plan

This commit is contained in:
2026-03-27 16:25:14 +01:00
parent 438079111c
commit 683470a7b1
3 changed files with 644 additions and 2 deletions
+5 -2
View File
@@ -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 | - |
@@ -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)"
---
<objective>
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.
</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
@synth/config.go
@classify/types.go
@synth/bank_test.go
@synth/config_test.go
@config/config_test.go
@classify/classifier_test.go
<interfaces>
<!-- Key types and contracts the executor needs -->
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
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Add 21 ClassFreqConfigs entries and update AllClasses()</name>
<files>synth/config.go, classify/types.go</files>
<read_first>synth/config.go, classify/types.go</read_first>
<action>
**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.
</action>
<verify>
<automated>cd /home/dev/workspace/yoloyolo && go build ./synth/... ./classify/...</automated>
</verify>
<acceptance_criteria>
- `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
</acceptance_criteria>
<done>ClassFreqConfigs has 35 entries with correct Hz/waveform/pan/group values. AllClasses() returns 35 entries including LDAP/Kerberos/Syslog. Both packages compile.</done>
</task>
<task type="auto">
<name>Task 2: Fix all hardcoded count assertions in tests</name>
<files>synth/bank_test.go, config/config_test.go, classify/classifier_test.go</files>
<read_first>synth/bank_test.go, config/config_test.go, classify/classifier_test.go</read_first>
<action>
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.
</action>
<verify>
<automated>cd /home/dev/workspace/yoloyolo && go test ./synth/... ./classify/... ./config/...</automated>
</verify>
<acceptance_criteria>
- `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
</acceptance_criteria>
<done>All test suites pass with 35 classes. No hardcoded counts of 14 or 32 remain in test assertions. TestNewBankHas14Layers renamed to TestNewBankHasAllLayers.</done>
</task>
</tasks>
<verification>
```bash
cd /home/dev/workspace/yoloyolo && go test ./...
```
All tests pass. ClassFreqConfigs has 35 entries matching AllClasses().
</verification>
<success_criteria>
- `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
</success_criteria>
<output>
After completion, create `.planning/phases/11-synthesis-and-config-layer/11-01-SUMMARY.md`
</output>
@@ -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"
---
<objective>
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.
</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/11-synthesis-and-config-layer/11-01-SUMMARY.md
@config/config.go
@config/config_test.go
<interfaces>
<!-- Key types and contracts from Plan 01 output -->
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
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Add [groups] TOML support and refactor PrintConfig for group headers</name>
<files>config/config.go</files>
<read_first>config/config.go</read_first>
<action>
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.
</action>
<verify>
<automated>cd /home/dev/workspace/yoloyolo && go build ./config/...</automated>
</verify>
<acceptance_criteria>
- `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
</acceptance_criteria>
<done>PrintConfig emits group-ordered output with section headers. rawConfig has Groups field. applyGroupOverrides function exists and is called in Load(). Compiles successfully.</done>
</task>
<task type="auto">
<name>Task 2: Add tests for group headers, group reassignment, and unknown class warning</name>
<files>config/config_test.go</files>
<read_first>config/config_test.go, config/config.go</read_first>
<action>
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.)
</action>
<verify>
<automated>cd /home/dev/workspace/yoloyolo && go test ./config/... -v -run "TestPrintConfigGroup|TestLoadGroup"</automated>
</verify>
<acceptance_criteria>
- `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)
</acceptance_criteria>
<done>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.</done>
</task>
</tasks>
<verification>
```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.
</verification>
<success_criteria>
- `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
</success_criteria>
<output>
After completion, create `.planning/phases/11-synthesis-and-config-layer/11-02-SUMMARY.md`
</output>