416 lines
16 KiB
Markdown
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>
|