Files
yoloyolo/.planning/milestones/v1.2-phases/11-synthesis-and-config-layer/11-02-PLAN.md
T
gurixandClaude Opus 4.6 494385b528 chore: archive v1.2 milestone — Extended Protocol Coverage
35 traffic classes across 9 protocol families shipped. Archives
ROADMAP, REQUIREMENTS, and phase directories to milestones/v1.2-*.
Updates README with new protocol families, sound design table,
and [groups] TOML config documentation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:48:53 +01:00

416 lines
16 KiB
Markdown

---
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>