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>
16 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 11-synthesis-and-config-layer | 02 | execute | 2 |
|
|
true |
|
|
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.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_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.goFrom synth/config.go (after Plan 01):
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):
func AllClasses() []TrafficClass // returns 35 entries including LDAP/Kerberos/Syslog
From config/config.go (current):
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
1. Add Groups field to rawConfig struct (per D-07):
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:
// 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:
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:
// 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:
// 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/...
<acceptance_criteria>
grep "groupOrder" config/config.goreturns the canonical group order slicegrep "applyGroupOverrides" config/config.goreturns the function definitiongrep 'Groups map\[string\]string' config/config.goshows the new rawConfig fieldgrep "# User-defined" config/config.goshows the user-defined section headergo build ./config/...succeeds </acceptance_criteria> PrintConfig emits group-ordered output with section headers. rawConfig has Groups field. applyGroupOverrides function exists and is called in Load(). Compiles successfully.
TestPrintConfigGroupHeaders -- Verifies GRP-02 group header output:
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:
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:
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:
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"
<acceptance_criteria>
go test ./config/... -run TestPrintConfigGroupHeaderspassesgo test ./config/... -run TestLoadGroupOverridepassesgo test ./config/... -run TestLoadGroupUnknownClasspassesgo test ./config/... -run TestPrintConfigGroupReassignmentpassesgo test ./config/...all tests pass (no regressions) </acceptance_criteria> 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.
Manual verification: go run . --print-config shows classes organized by group with # Infrastructure, # Web, # Mail, # Remote Access, # File Transfer, # Database, # VoIP, # Unknown section headers.
<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>