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>
This commit is contained in:
2026-03-27 16:48:53 +01:00
co-authored by Claude Opus 4.6
parent 0a4d48c9c1
commit 494385b528
42 changed files with 290 additions and 166 deletions
@@ -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,103 @@
---
phase: 11
plan: 01
subsystem: synth, classify
tags: [frequency-config, traffic-classes, test-fixes, data-layer]
dependency_graph:
requires: [classify/types.go TrafficClass constants from Phase 10]
provides: [ClassFreqConfigs with 35 entries, AllClasses() returning 35 entries]
affects: [synth/bank.go, config/config.go, config/config_test.go, synth/bank_test.go]
tech_stack:
added: []
patterns: [WaveformPresetHarmonics for all new entries, len(classify.AllClasses()) for dynamic counts]
key_files:
created: []
modified:
- synth/config.go
- classify/types.go
- synth/bank_test.go
- config/config_test.go
- classify/classifier_test.go
decisions:
- "Use len(classify.AllClasses()) in synth and config tests for dynamic count validation"
- "LDAP/Kerberos/Syslog placed in Infrastructure section of AllClasses() after SNMP"
- "All 21 new ClassFreqConfigs entries use WaveformPresetHarmonics() per D-01 decision"
metrics:
duration: ~8min
completed: "2026-03-27"
tasks: 2
files_modified: 5
---
# Phase 11 Plan 01: Frequency Config Data Layer Summary
**One-liner:** Added 21 ClassFreqConfigs entries (mDNS through Syslog) and expanded AllClasses() to 35 with LDAP/Kerberos/Syslog, fixing all hardcoded count assertions across synth, config, and classify test files.
## What Was Built
### Task 1: Add 21 ClassFreqConfigs entries and update AllClasses()
**synth/config.go** — Added 21 new `FreqConfig` entries to `ClassFreqConfigs` map, organized by protocol family:
| Family | Classes | Hz Range | Waveform |
|--------|---------|----------|----------|
| Infrastructure additions | mDNS, SSDP, SNMP | 93-118 Hz | Triangle |
| Web addition | QUIC | 190 Hz | Sawtooth |
| Mail additions | IMAP, POP3, SMTP-sub | 241-305 Hz | Triangle |
| Remote Access additions | RDP, Telnet, VNC | 385-485 Hz | Square |
| File Transfer additions | FTP, SMB, TFTP | 545-687 Hz | Square |
| Database additions | MySQL, PostgreSQL, Redis, MongoDB | 1543-2182 Hz | Sawtooth |
| VoIP | SIP | 2449 Hz | Sine |
| Infrastructure auto-assigned | LDAP, Kerberos, Syslog | 2950-3250 Hz | Triangle |
All 21 entries use `WaveformPresetHarmonics()` per D-01. Total map: 35 entries.
**classify/types.go** — Updated `AllClasses()`:
- Added `ClassLDAP`, `ClassKerberos`, `ClassSyslog` to Infrastructure section (after ClassSNMP)
- Updated doc comment: removed Phase 11 exclusion note, now says "AllClasses returns all known traffic classes in display order."
- Now returns 35 entries (was 32)
### Task 2: Fix all hardcoded count assertions
| File | Change |
|------|--------|
| synth/bank_test.go | Renamed `TestNewBankHas14Layers` to `TestNewBankHasAllLayers`; `!= 14``!= len(classify.AllClasses())` |
| config/config_test.go | 4 occurrences of `!= 14``!= len(classify.AllClasses())`; TestPrintConfigContainsAllClasses: hardcoded 14-class list → `classify.AllClasses()` loop |
| classify/classifier_test.go | `want 32``want 35` in TestAllClassesCount |
## Verification Results
```
go test ./...
ok github.com/netsynth/netsynth/aggregate
ok github.com/netsynth/netsynth/capture
ok github.com/netsynth/netsynth/classify
ok github.com/netsynth/netsynth/cmd/netsynth
ok github.com/netsynth/netsynth/config
ok github.com/netsynth/netsynth/encode
ok github.com/netsynth/netsynth/synth
```
All 7 packages pass.
## Commits
| Task | Commit | Description |
|------|--------|-------------|
| 1 | 1556ce8 | feat(11-01): add 21 ClassFreqConfigs entries and include LDAP/Kerberos/Syslog in AllClasses() |
| 2 | 35d8915 | fix(11-01): update hardcoded count assertions to reflect 35 traffic classes |
## Deviations from Plan
**1. [Rule 3 - Blocker] Worktree was at stale commit missing Phase 8-10 changes**
- **Found during:** Pre-execution setup
- **Issue:** Worktree branch `worktree-agent-af29ad97` was at commit `8e565ec` (milestone v1.2 roadmap docs), 43 commits behind `master`. The target files (synth/config.go, classify/types.go) were in their Phase 7 state.
- **Fix:** Ran `git merge master --no-edit` (fast-forward) to bring worktree to current HEAD `683470a`.
- **Files modified:** All files (fast-forward merge, no conflicts)
- **Commit:** Not a separate commit — prerequisite action before Task 1
## Known Stubs
None — all 35 ClassFreqConfigs entries have complete Hz, waveform, pan, and group values. No placeholder data.
## Self-Check: PASSED
@@ -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>
@@ -0,0 +1,97 @@
---
phase: 11
plan: 02
subsystem: config
tags: [print-config, group-headers, toml-groups, user-facing-output]
dependency_graph:
requires: [synth/config.go FreqConfig.Group field from Phase 11 Plan 01, classify/types.go AllClasses() with 35 entries]
provides: [Group-ordered PrintConfig output, [groups] TOML support, applyGroupOverrides function]
affects: [config/config.go, config/config_test.go]
tech_stack:
added: []
patterns: [rawConfig Groups field for TOML [groups] table, groupOrder canonical slice for section ordering]
key_files:
created: []
modified:
- config/config.go
- config/config_test.go
decisions:
- "Non-canonical group names (user-invented via [groups]) emitted after canonical groups in alphabetical order"
- "builtinByGroup built from result.FreqCfgs[cls].Group (effective group after reassignment) not from synth defaults"
metrics:
duration: ~5min
completed: "2026-03-27"
tasks: 2
files_modified: 2
---
# Phase 11 Plan 02: Group-Ordered PrintConfig and [groups] TOML Support Summary
**One-liner:** Refactored PrintConfig to emit group section headers (Infrastructure, Web, Mail, Remote Access, File Transfer, Database, Discovery, VoIP, Unknown) with classes sorted by ascending BaseHz, and added [groups] TOML table support for user-defined protocol-to-group reassignment.
## What Was Built
### Task 1: Add [groups] TOML support and refactor PrintConfig for group headers
**config/config.go** — Three changes:
**1. Groups field on rawConfig:**
```go
type rawConfig struct {
Sounds map[string]SoundOverride `toml:"sounds"`
Rules []RawRule `toml:"rules"`
Groups map[string]string `toml:"groups"`
}
```
**2. applyGroupOverrides function** — overlays [groups] reassignments onto FreqConfig.Group in-place. Unknown class names emit a stderr warning and are skipped (not an error). Unknown group names are silently accepted (users can invent custom groups). Called in Load() after merge().
**3. PrintConfig refactored** — replaced flat AllClasses() iteration with:
- `groupOrder` canonical slice: `["Infrastructure", "Web", "Mail", "Remote Access", "File Transfer", "Database", "Discovery", "VoIP", "Unknown"]`
- Built-in classes grouped by their effective Group field, sorted ascending by BaseHz within each group
- Section headers emitted as `# GroupName\n\n`
- Non-canonical group names (user-invented) emitted after canonical groups in alphabetical order
- User-defined classes (not in AllClasses()) emitted under `# User-defined` section
### Task 2: Add tests for group headers, group reassignment, and unknown class warning
**config/config_test.go** — 4 new test functions:
| Test | What It Covers |
|------|---------------|
| `TestPrintConfigGroupHeaders` | GRP-02: all 8 populated group headers present in canonical order |
| `TestLoadGroupOverride` | GRP-03: [groups] reassigns IMAP from Mail to Web, Hz/waveform unchanged |
| `TestLoadGroupUnknownClass` | D-09: unknown class in [groups] produces no error, map size unchanged |
| `TestPrintConfigGroupReassignment` | GRP-03: PrintConfig places reassigned IMAP between Web and Mail headers |
## Verification Results
```
go test ./...
ok github.com/netsynth/netsynth/aggregate (cached)
ok github.com/netsynth/netsynth/capture (cached)
ok github.com/netsynth/netsynth/classify
ok github.com/netsynth/netsynth/cmd/netsynth
ok github.com/netsynth/netsynth/config
ok github.com/netsynth/netsynth/encode
ok github.com/netsynth/netsynth/synth
```
All 7 packages pass.
## Commits
| Task | Commit | Description |
|------|--------|-------------|
| 1 | 374282e | feat(11-02): add [groups] TOML support and group-ordered PrintConfig |
| 2 | 7bf3ea1 | test(11-02): add group header and reassignment tests (GRP-02, GRP-03) |
## Deviations from Plan
None - plan executed exactly as written.
## Known Stubs
None — PrintConfig group output is fully wired to FreqConfig.Group field populated in Phase 11 Plan 01. No placeholder data.
## Self-Check: PASSED
@@ -0,0 +1,113 @@
# Phase 11: Synthesis and Config Layer - Context
**Gathered:** 2026-03-27
**Status:** Ready for planning
<domain>
## Phase Boundary
Add ClassFreqConfigs entries for all 18 new classes in AllClasses() plus 3 LDAP/Kerberos/Syslog classes (adding them to AllClasses() too). Update `PrintConfig` to output classes grouped by family with section header comments. Add `[groups]` TOML config support for users to reassign protocols to different sound families. Fix all broken synth/config tests.
</domain>
<decisions>
## Implementation Decisions
### ClassFreqConfigs Entries
- **D-01:** Add ClassFreqConfigs entries for all 18 new classes currently in AllClasses(). Use Hz values, waveforms, pans, and groups from the Phase 9 frequency allocation table comment in `synth/config.go` lines 74-110. Use `WaveformPresetHarmonics()` for all new entries (not hand-tuned harmonics).
- **D-02:** Add ClassLDAP, ClassKerberos, ClassSyslog to AllClasses() in `classify/types.go`. Create ClassFreqConfigs entries for them using `autoAssignFreq`-derived Hz values (FNV hash in [2500, 4000] Hz range). Their group is "Infrastructure", waveform is Triangle (matching the Infrastructure family pattern).
- **D-03:** After D-01 and D-02, AllClasses() returns 35 entries (32 + 3). TestAllClassesCount updated to 35.
### PrintConfig Group Headers (GRP-02)
- **D-04:** PrintConfig groups classes by their Group field value. Each group gets a comment header line: `# <Group>` followed by a blank line, then all classes in that group. Groups are ordered: Infrastructure, Web, Mail, Remote Access, File Transfer, Database, Discovery, VoIP, Unknown.
- **D-05:** Within each group, classes are ordered by ascending BaseHz (matching the frequency allocation table order).
- **D-06:** User-defined classes (not in AllClasses but in FreqCfgs) are emitted after all built-in groups under a "# User-defined" section header.
### TOML Groups Config (GRP-03)
- **D-07:** Users define group reassignments in TOML with a `[groups]` table using simple key-value pairs: `IMAP = "Web"` reassigns IMAP from Mail to Web group. The key is the TrafficClass string value, the value is the target group name.
- **D-08:** Group reassignment only affects `--print-config` output grouping and the Group field in FreqConfig. It does NOT change frequency, waveform, or pan — those stay as designed. PrintConfig reflects the reassignment.
- **D-09:** Unknown group names in `[groups]` config are accepted (user can invent custom group names). Unknown class names produce a warning (same pattern as `[sounds.X]` with unknown class).
### Claude's Discretion
- Exact Hz values for LDAP, Kerberos, Syslog (computed from autoAssignFreq FNV hash)
- Pan positions for LDAP, Kerberos, Syslog
- Test structure for new ClassFreqConfigs entries and PrintConfig group output
- Whether to add `[groups]` to rawConfig struct as `map[string]string` or a custom type
- How to handle group reassignment in the merge/load pipeline
### Folded Todos
- **"Expand Traffic Classes"** (from `.planning/todos/pending/001-expand-traffic-classes.md`) — Phase 11 completes the synthesis side of this request. Constants/rules were added in Phase 10; now all classes get sound configurations.
</decisions>
<canonical_refs>
## Canonical References
**Downstream agents MUST read these before planning or implementing.**
### Synth Package (primary modification target)
- `synth/config.go``ClassFreqConfigs` map (add 21 new entries), frequency allocation table comment (lines 74-110), `FreqConfig` struct, `WaveformPresetHarmonics` function
- `synth/config_test.go``TestAllClassesHaveConfig`, `TestClassFreqConfigsMatchAllClasses`, `TestFrequenciesInRange`, `TestFrequenciesUnique`, `TestNewBankHas14Layers`
### Config Package (PrintConfig + groups)
- `config/config.go``PrintConfig` function (lines 270-336), `rawConfig` struct (line 38-41), `LoadResult` struct, `merge` function, `autoAssignFreq` function, `addAutoFreqEntries`
- `config/config_test.go``TestLoadAllDefaultsPresent`, `TestPrintConfigOutput`
### Classification Package (AllClasses update)
- `classify/types.go` — AllClasses() function, TrafficClass constants (add LDAP/Kerberos/Syslog to AllClasses())
- `classify/classifier_test.go``TestAllClassesCount` (update from 32 to 35)
### Requirements
- `.planning/REQUIREMENTS.md` — GRP-02, GRP-03
- `.planning/ROADMAP.md` — Phase 11 success criteria
### Prior Phase Context
- `.planning/phases/09-frequency-design-and-group-architecture/09-CONTEXT.md` — Frequency design decisions, waveform-per-family strategy
- `.planning/phases/10-classification-layer/10-CONTEXT.md` — PROTO-08 frequency strategy (D-01: autoAssignFreq for LDAP/Kerberos/Syslog)
</canonical_refs>
<code_context>
## Existing Code Insights
### Reusable Assets
- `WaveformPresetHarmonics(wt, baseHz, sampleRate)` — generates harmonics for any waveform at any frequency; use for all 21 new ClassFreqConfigs entries
- `autoAssignFreq(className)` — FNV hash into [2500, 4000] Hz; use for LDAP/Kerberos/Syslog Hz values
- `classAnnotation(cls, cfg, autoClasses)` — already handles "default", "override", "auto-assigned" annotations
- `copyDefaults()` and `merge()` — existing config merge pipeline to extend with group support
### Established Patterns
- ClassFreqConfigs entries follow: `classify.ClassX: {BaseHz, WaveformType, Harmonics: WaveformPresetHarmonics(...), Pan, Group}` pattern
- PrintConfig iterates AllClasses() for built-ins, then sorts user-defined separately
- rawConfig uses TOML struct tags for decoding; adding `[groups]` follows same pattern
- SoundOverride uses pointer fields for partial overrides
### Integration Points
- `config.Load()` returns `LoadResult` with `FreqCfgs` map — group reassignments must be applied before returning
- `PrintConfig` reads `FreqCfgs` and `AllClasses()` — group headers derived from `FreqConfig.Group` field
- `NewBank()` in `synth/bank.go` creates layers from `ClassFreqConfigs` — all new entries will get synthesis layers automatically
- `TestNewBankHas14Layers` in `synth/bank_test.go` — name is stale, needs update to reflect 35 classes
</code_context>
<specifics>
## Specific Ideas
- The frequency allocation table comment in `synth/config.go` (lines 74-110) is the authoritative source for all Hz, waveform, group, and pan values for the 18 table-designed classes
- LDAP/Kerberos/Syslog get Infrastructure group + Triangle waveform (matching existing Infrastructure family) but their Hz comes from autoAssignFreq, not the table
- PrintConfig currently has no group awareness — it just lists all classes in AllClasses() order. The refactor adds group-based iteration with comment headers
- `go test ./...` must pass fully after Phase 11 — this is the first time since Phase 10 that the full test suite should be green
</specifics>
<deferred>
## Deferred Ideas
None — discussion stayed within phase scope.
</deferred>
---
*Phase: 11-synthesis-and-config-layer*
*Context gathered: 2026-03-27*
@@ -0,0 +1,62 @@
# Phase 11: Synthesis and Config Layer - Discussion Log
> **Audit trail only.** Do not use as input to planning, research, or execution agents.
> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
**Date:** 2026-03-27
**Phase:** 11-synthesis-and-config-layer
**Areas discussed:** Group header format, TOML groups schema, LDAP/Kerberos/Syslog handling
**Mode:** --auto (all decisions auto-selected)
---
## Group Header Format in PrintConfig
| Option | Description | Selected |
|--------|-------------|----------|
| Comment headers with blank line separator | `# Mail` followed by blank line, then classes. Matches existing comment patterns. | ✓ |
| Section dividers with dashes | `# --- Mail ---` style separator | |
| No headers (flat list) | Keep current flat listing, rely on frequency ordering | |
**User's choice:** [auto] Comment headers with blank line separator (recommended default)
**Notes:** Matches existing `# Classification rules` comment pattern in PrintConfig output
---
## TOML Groups Config Schema
| Option | Description | Selected |
|--------|-------------|----------|
| Simple key-value map | `[groups]` with `IMAP = "Web"` pairs. Simplest approach. | ✓ |
| Nested table | `[groups.Mail]` with `members = ["IMAP", "POP3"]` — more structured but heavier | |
| Array of tables | `[[groups]]` with name/members fields — most flexible but overkill | |
**User's choice:** [auto] Simple key-value map (recommended default)
**Notes:** Consistent with existing `[sounds.X]` pattern. Key = class name, value = target group.
---
## LDAP/Kerberos/Syslog Handling
| Option | Description | Selected |
|--------|-------------|----------|
| Add to AllClasses() + ClassFreqConfigs | Complete the full set, fix broken tests. autoAssignFreq-derived Hz. | ✓ |
| Keep out of AllClasses() | Leave as constants-only, no synthesis. Tests remain broken. | |
| Add to ClassFreqConfigs only | Add configs but don't add to AllClasses(). Partial fix. | |
**User's choice:** [auto] Add to AllClasses() + ClassFreqConfigs (recommended default)
**Notes:** Completes the full 35-class set. Fixes TestAllClassesHaveConfig and related tests.
---
## Claude's Discretion
- Exact Hz values for LDAP/Kerberos/Syslog (autoAssignFreq FNV hash)
- Pan positions for LDAP/Kerberos/Syslog
- Test structure and naming updates
- rawConfig struct extension for `[groups]`
- Group reassignment pipeline in merge/load
## Deferred Ideas
None — discussion stayed within phase scope.
@@ -0,0 +1,412 @@
# Phase 11: Synthesis and Config Layer - Research
**Researched:** 2026-03-27
**Domain:** Go — synth config completion, PrintConfig group headers, TOML [groups] table
**Confidence:** HIGH
## Summary
Phase 11 completes the synthesis layer for all 35 traffic classes and adds group-aware output to `PrintConfig`. The work is entirely within `synth/config.go`, `config/config.go`, and `classify/types.go` — no new packages, no external dependencies beyond what is already in go.mod. The frequency allocation table in `synth/config.go` lines 74110 is the authoritative source of truth for all Hz, waveform, group, and pan values for the 18 table-designed classes. LDAP, Kerberos, and Syslog get their Hz values from `autoAssignFreq` (verified by running the FNV hash: LDAP=2950 Hz, Kerberos=3250 Hz, Syslog=3050 Hz) and use Triangle waveform / Infrastructure group, matching existing Infrastructure family members.
The test suite is currently broken in `config` and `synth` packages because `ClassFreqConfigs` only has 14 entries while `AllClasses()` returns 32. After Phase 11, `AllClasses()` returns 35 and `ClassFreqConfigs` must match exactly. Multiple existing test hardcodes (`want 14`, `TestNewBankHas14Layers`, `TestAllClassesCount want 32`) need updating to 35.
The `[groups]` TOML feature requires adding a `Groups map[string]string` field to `rawConfig`, wiring it through `Load()`, storing the reassignments in `LoadResult`, applying them in `PrintConfig`, and adding a warning for unknown class names. No existing config pipeline stages need structural changes — group reassignment is a post-merge overlay on FreqConfig.Group fields.
**Primary recommendation:** Execute in three sequential sub-tasks: (1) add 21 ClassFreqConfigs entries + update AllClasses() + fix count tests, (2) refactor PrintConfig for group headers, (3) add [groups] TOML support + update tests.
---
<user_constraints>
## User Constraints (from CONTEXT.md)
### Locked Decisions
- **D-01:** Add ClassFreqConfigs entries for all 18 new classes currently in AllClasses(). Use Hz values, waveforms, pans, and groups from the Phase 9 frequency allocation table comment in `synth/config.go` lines 74-110. Use `WaveformPresetHarmonics()` for all new entries (not hand-tuned harmonics).
- **D-02:** Add ClassLDAP, ClassKerberos, ClassSyslog to AllClasses() in `classify/types.go`. Create ClassFreqConfigs entries for them using `autoAssignFreq`-derived Hz values (FNV hash in [2500, 4000] Hz range). Their group is "Infrastructure", waveform is Triangle (matching the Infrastructure family pattern).
- **D-03:** After D-01 and D-02, AllClasses() returns 35 entries (32 + 3). TestAllClassesCount updated to 35.
- **D-04:** PrintConfig groups classes by their Group field value. Each group gets a comment header line: `# <Group>` followed by a blank line, then all classes in that group. Groups are ordered: Infrastructure, Web, Mail, Remote Access, File Transfer, Database, Discovery, VoIP, Unknown.
- **D-05:** Within each group, classes are ordered by ascending BaseHz (matching the frequency allocation table order).
- **D-06:** User-defined classes (not in AllClasses but in FreqCfgs) are emitted after all built-in groups under a "# User-defined" section header.
- **D-07:** Users define group reassignments in TOML with a `[groups]` table using simple key-value pairs: `IMAP = "Web"` reassigns IMAP from Mail to Web group. The key is the TrafficClass string value, the value is the target group name.
- **D-08:** Group reassignment only affects `--print-config` output grouping and the Group field in FreqConfig. It does NOT change frequency, waveform, or pan — those stay as designed. PrintConfig reflects the reassignment.
- **D-09:** Unknown group names in `[groups]` config are accepted (user can invent custom group names). Unknown class names produce a warning (same pattern as `[sounds.X]` with unknown class).
### Claude's Discretion
- Exact Hz values for LDAP, Kerberos, Syslog (computed from autoAssignFreq FNV hash)
- Pan positions for LDAP, Kerberos, Syslog
- Test structure for new ClassFreqConfigs entries and PrintConfig group output
- Whether to add `[groups]` to rawConfig struct as `map[string]string` or a custom type
- How to handle group reassignment in the merge/load pipeline
### Deferred Ideas (OUT OF SCOPE)
None — discussion stayed within phase scope.
</user_constraints>
---
<phase_requirements>
## Phase Requirements
| ID | Description | Research Support |
|----|-------------|------------------|
| GRP-02 | `--print-config` output organizes classes by group with section headers | PrintConfig refactor: iterate canonical group order, emit `# <Group>` header + blank line, sort within group by BaseHz ascending; user-defined classes under `# User-defined` |
| GRP-03 | Users can define `[groups]` in TOML config to reassign protocols to different sound families | Add `Groups map[string]string` to rawConfig; apply after merge in Load(); warn on unknown class name; store in LoadResult; PrintConfig reads Group field from FreqConfig |
</phase_requirements>
---
## Standard Stack
Phase 11 uses only packages already in go.mod. No new dependencies.
### Core (already in go.mod)
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| `github.com/BurntSushi/toml` | existing | TOML decode — `map[string]string` for `[groups]` table | Already used; `md.Undecoded()` handles unknown key detection |
| `github.com/netsynth/netsynth/classify` | local | TrafficClass constants, AllClasses() | Local package; phase modifies it |
| `github.com/netsynth/netsynth/synth` | local | FreqConfig, ClassFreqConfigs, WaveformPresetHarmonics | Local package; phase adds 21 entries |
**No installation needed.** `go test ./...` is the only verification command.
---
## Architecture Patterns
### Pattern 1: ClassFreqConfigs Entry Structure
**What:** Each entry in `ClassFreqConfigs` follows a strict pattern — BaseHz from the frequency table, WaveformType constant, Harmonics generated by `WaveformPresetHarmonics`, Pan from the table, Group string.
**When to use:** For all 18 table-designed new entries (D-01) and 3 auto-assigned entries (D-02).
**Example (from existing code):**
```go
classify.ClassIMAP: {
BaseHz: 241.0,
WaveformType: WaveformTriangle,
Harmonics: WaveformPresetHarmonics(WaveformTriangle, 241.0, SampleRate),
Pan: 0.3,
Group: "Mail",
},
```
### Pattern 2: autoAssignFreq for LDAP/Kerberos/Syslog
**What:** FNV-32a hash into [2500, 4000] Hz range. Values are deterministic — computed once and hardcoded in the entry.
**Verified computed values:**
- LDAP: 2950.0 Hz
- Kerberos: 3250.0 Hz
- Syslog: 3050.0 Hz
Pan positions (Claude's discretion): assign spread within [-0.3, 0.3] range — LDAP=-0.2, Kerberos=0.0, Syslog=0.2 keeps them center-ish (Infrastructure family is already spread -0.3 to +0.3 at low frequencies; these are high-frequency so center-cluster is appropriate).
### Pattern 3: AllClasses() Update (classify/types.go)
**What:** Add ClassLDAP, ClassKerberos, ClassSyslog inside the Infrastructure block in AllClasses(). The comment guard `// D-01: no ClassFreqConfigs until Phase 11` is removed.
**Ordering within Infrastructure:** Append after ClassSNMP (highest-Hz member at 118 Hz) since LDAP/Kerberos/Syslog are at 2950/3250/3050 Hz — technically they sort higher but within their family group they follow table order.
### Pattern 4: PrintConfig Group-Ordered Iteration (GRP-02)
**What:** Replace the current flat `classify.AllClasses()` iteration with a canonical-group-ordered iteration. Build a `groupOrder []string` slice with the 9 canonical groups; for each group collect classes from AllClasses() whose `FreqCfgs[cls].Group == group`, sort by BaseHz, emit group header + entries.
**Canonical group order (D-04):**
```
Infrastructure, Web, Mail, Remote Access, File Transfer, Database, Discovery, VoIP, Unknown
```
**Implementation approach:**
```go
// groupOrder defines canonical display order for --print-config
var groupOrder = []string{
"Infrastructure", "Web", "Mail", "Remote Access",
"File Transfer", "Database", "Discovery", "VoIP", "Unknown",
}
// Build group -> []TrafficClass index from AllClasses()
// Sort each group slice by FreqCfgs[cls].BaseHz ascending (D-05)
// For each group: emit "# <group>\n\n" then entries
// After all built-in groups: user-defined classes under "# User-defined\n\n"
```
**Critical detail:** The sort must use `result.FreqCfgs[cls].BaseHz` (the effective config, post-merge) not `synth.ClassFreqConfigs[cls].BaseHz`, so user frequency overrides are reflected in the sort order. This is unlikely to matter in practice but is correct.
### Pattern 5: TOML [groups] Support (GRP-03)
**rawConfig struct extension:**
```go
type rawConfig struct {
Sounds map[string]SoundOverride `toml:"sounds"`
Rules []RawRule `toml:"rules"`
Groups map[string]string `toml:"groups"` // key=TrafficClass string, val=group name
}
```
Using `map[string]string` (Claude's discretion) is the simplest approach — TOML decodes `[groups]` as a string map naturally. No custom type needed.
**LoadResult extension:**
```go
type LoadResult struct {
FreqCfgs map[classify.TrafficClass]synth.FreqConfig
UserRules []classify.Rule
ConfigPath string
AutoClasses map[classify.TrafficClass]bool
GroupOverrides map[classify.TrafficClass]string // NEW: class -> reassigned group name
}
```
**applyGroupOverrides function (new):** After `merge()` in `Load()`, iterate `raw.Groups`; for each key=className, val=groupName: if className is known (exists in freqCfgs), update `cfg.Group = groupName` and store back; if unknown, emit warning to stderr (D-09). The warning uses the same pattern as unknown [sounds.X] classes.
**Alternative approach:** Apply group reassignment directly inside PrintConfig by reading `result.GroupOverrides` map on-the-fly without mutating FreqConfig. This avoids touching LoadResult but makes PrintConfig depend on a new field anyway. Mutating FreqConfig.Group is cleaner because classAnnotation and other consumers see a consistent view.
### Pattern 6: TOML md.Undecoded() and the [groups] map
**Critical:** `BurntSushi/toml` decodes map fields without flagging individual map keys as "undecoded" — unknown class names in `[groups]` will NOT be caught by `md.Undecoded()`. This is the same behavior as `[sounds.*]` (unknown class names are silently accepted at decode time and validated in `merge()`). The warning for unknown class names in `[groups]` must be implemented in the new `applyGroupOverrides` function, not in `parseFile`. This matches D-09 exactly.
### Anti-Patterns to Avoid
- **Hand-computing harmonics for new entries:** Use `WaveformPresetHarmonics()` — don't write `[]HarmonicDef{{1, 1.0}, {2, 0.5}, ...}` manually for table-designed entries.
- **Sorting AllClasses() output by BaseHz globally:** The table order in AllClasses() is the authoritative display order within families. The sort in PrintConfig must use group-then-BaseHz, not a flat sort.
- **Adding [groups] validation to parseFile:** `md.Undecoded()` cannot catch unknown string map keys; validation must be in `applyGroupOverrides`.
- **Mutating synth.ClassFreqConfigs:** copyDefaults() produces a working copy; all mutations go there, never to the package-level map.
---
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Harmonic series for new waveform entries | Manual `[]HarmonicDef` slices | `WaveformPresetHarmonics(wt, baseHz, SampleRate)` | Already computes bandlimited series; manual values would drift if SampleRate changes |
| FNV hash for LDAP/Kerberos/Syslog Hz | Custom hash | `autoAssignFreq("LDAP")` output hardcoded as 2950.0 | Values are pre-computed; hardcoding eliminates runtime dependency on config package from synth package |
| TOML group reassignment validation | Custom struct with Validate() | `map[string]string` + warning in applyGroupOverrides | Matches existing pattern for [sounds.*] unknown classes |
---
## Runtime State Inventory
Step 2.5: SKIPPED — this is not a rename/refactor/migration phase.
---
## Environment Availability Audit
Step 2.6: SKIPPED — phase is purely code changes within existing Go packages. No external tools, services, databases, or CLI utilities beyond the existing `go` toolchain are required. The Go toolchain is already confirmed working (tests ran above).
---
## Common Pitfalls
### Pitfall 1: Stale hardcoded counts in tests
**What goes wrong:** `TestNewBankHas14Layers` expects 14 layers; `TestLoadNoConfig`/`TestLoadUnknownClass`/`TestLoadAllDefaultsPresent`/`TestLoadNoConfigReturnsLoadResult` all assert `len(cfgs) == 14`; `TestAllClassesCount` asserts 32. After adding 21 ClassFreqConfigs entries and 3 AllClasses entries these fail.
**Why it happens:** Tests were written against the Phase 9 baseline of 14 built-in entries and never updated.
**How to avoid:** Update all hardcoded count assertions to 35 in a single pass after D-01 and D-02 are complete. The specific files and line numbers:
- `synth/bank_test.go:12``want 14``want 35`
- `synth/bank_test.go:13``len(b.layers) != 14``!= 35`
- `config/config_test.go:120``want 14``want 35`
- `config/config_test.go:149``want 14 (BOGUS should not appear)``want 35`
- `config/config_test.go:185``want 14``want 35`
- `config/config_test.go:617``want 14``want 35`
- `classify/classifier_test.go:459``want 32``want 35`
**Warning signs:** Any test that hardcodes a count of 14 or 32.
### Pitfall 2: TestPrintConfigContainsAllClasses class list is stale
**What goes wrong:** `config/config_test.go:407416` lists exactly 14 class name strings. After Phase 11, AllClasses() has 35 entries; the test should either check all 35 or be replaced by a loop over `classify.AllClasses()`.
**How to avoid:** Replace the hardcoded `classNames` slice with `for _, cls := range classify.AllClasses()` during PrintConfig test updates.
### Pitfall 3: PrintConfig group-sort uses default Hz, not effective Hz
**What goes wrong:** If group-sort code reads `synth.ClassFreqConfigs[cls].BaseHz` instead of `result.FreqCfgs[cls].BaseHz`, user frequency overrides don't affect sort position. The output is still correct for default configs but fails when the user overrides a frequency in [sounds.*].
**How to avoid:** Always sort from `result.FreqCfgs[cls].BaseHz`.
### Pitfall 4: [groups] map keys not caught by md.Undecoded()
**What goes wrong:** `parseFile` uses `md.Undecoded()` to catch field typos. TOML map keys are all valid decode targets by definition — `md.Undecoded()` will be empty even if `[groups]` contains `IMAPtypo = "Web"`. The warning must come from `applyGroupOverrides`.
**How to avoid:** Implement unknown-class warning in `applyGroupOverrides`, not in `parseFile` or `validate`.
### Pitfall 5: TestClassFreqConfigsMatchAllClasses fails if counts diverge
**What goes wrong:** `synth/config_test.go:57` checks `len(synth.ClassFreqConfigs) == len(classify.AllClasses())`. If the implementer adds all 21 ClassFreqConfigs entries but only adds 2 of the 3 new AllClasses entries, this test fails in a confusing way.
**How to avoid:** D-01 and D-02 must be completed atomically — add all 21 ClassFreqConfigs entries and all 3 AllClasses entries in a single task.
### Pitfall 6: "File Transfer" group name has a space
**What goes wrong:** If the group sort map uses `"FileTransfer"` instead of `"File Transfer"` (matching the FreqConfig.Group string), Discovery and File Transfer classes end up under a catch-all or dropped entirely.
**How to avoid:** All group strings must exactly match those in `FreqConfig.Group`. Canonical list: `"Infrastructure"`, `"Web"`, `"Mail"`, `"Remote Access"`, `"File Transfer"`, `"Database"`, `"Discovery"`, `"VoIP"`, `"Unknown"`.
---
## Code Examples
### Complete frequency table for all 21 missing entries
From the allocation table in `synth/config.go` lines 74110 plus D-02 computed values:
```
// Infrastructure additions (Triangle, [93-118] Hz)
mDNS: 93 Hz, Triangle, pan=+0.3
SSDP: 105 Hz, Triangle, pan=-0.2
SNMP: 118 Hz, Triangle, pan=+0.2
// Web addition (Sawtooth)
QUIC: 190 Hz, Sawtooth, pan=-0.2
// Mail additions (Triangle)
IMAP: 241 Hz, Triangle, pan=+0.3
POP3: 271 Hz, Triangle, pan=+0.4
SMTP-sub: 305 Hz, Triangle, pan=+0.5
// Remote Access additions (Square)
RDP: 385 Hz, Square, pan=-0.6
Telnet: 432 Hz, Square, pan=-0.5
VNC: 485 Hz, Square, pan=-0.4
// File Transfer additions (Square)
FTP: 545 Hz, Square, pan=+0.5
SMB: 612 Hz, Square, pan=+0.6
TFTP: 687 Hz, Square, pan=+0.7
// Database additions (Sawtooth)
MySQL: 1543 Hz, Sawtooth, pan=-0.4
PostgreSQL: 1732 Hz, Sawtooth, pan=-0.2
Redis: 1944 Hz, Sawtooth, pan=+0.2
MongoDB: 2182 Hz, Sawtooth, pan=+0.4
// VoIP (Sine)
SIP: 2449 Hz, Sine, pan=0.0
// Infrastructure auto-assigned (Triangle) — D-02
LDAP: 2950 Hz, Triangle, pan=-0.2
Kerberos: 3250 Hz, Triangle, pan=0.0
Syslog: 3050 Hz, Triangle, pan=+0.2
```
### PrintConfig group header pattern (D-04)
```go
// groupOrder is the canonical display order for --print-config section headers.
var groupOrder = []string{
"Infrastructure", "Web", "Mail", "Remote Access",
"File Transfer", "Database", "Discovery", "VoIP", "Unknown",
}
// In PrintConfig, replace the flat AllClasses() loop with:
builtinByGroup := map[string][]classify.TrafficClass{}
for _, cls := range classify.AllClasses() {
cfg := result.FreqCfgs[cls]
grp := cfg.Group
builtinByGroup[grp] = append(builtinByGroup[grp], cls)
}
// Sort each group slice by BaseHz ascending (D-05)
for grp := range builtinByGroup {
sort.Slice(builtinByGroup[grp], func(i, j int) bool {
return result.FreqCfgs[builtinByGroup[grp][i]].BaseHz <
result.FreqCfgs[builtinByGroup[grp][j]].BaseHz
})
}
// Emit in canonical order
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 {
// ... existing per-class emit logic ...
}
}
```
### [groups] TOML apply pattern (D-07/D-08/D-09)
```go
// applyGroupOverrides overlays [groups] reassignments onto freqCfgs.Group in-place.
// Unknown class names produce a warning; unknown group names are silently accepted (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
}
}
```
Call site in `Load()` — after `merge()`:
```go
addAutoFreqEntries(freqCfgs, userRules, autoClasses)
merge(freqCfgs, raw.Sounds)
applyGroupOverrides(freqCfgs, raw.Groups) // NEW
```
---
## Validation Architecture
nyquist_validation is enabled (not false in config.json).
### Test Framework
| Property | Value |
|----------|-------|
| Framework | Go testing (stdlib) |
| Config file | none — `go test ./...` |
| Quick run command | `go test ./classify/... ./synth/... ./config/...` |
| Full suite command | `go test ./...` |
### Current Test Failures (baseline — must fix)
The following tests are currently failing and Phase 11 must make them green:
| Test | Package | Failure Cause | Fix Required |
|------|---------|---------------|--------------|
| `TestAllClassesHaveConfig` | `synth` | 18 new classes in AllClasses() missing from ClassFreqConfigs | Add 18+3 entries |
| `TestClassFreqConfigsMatchAllClasses` | `synth` | 14 != 32 | Add entries, update AllClasses |
| `TestNewBankHas14Layers` | `synth` | 14 != 32 + missing layers | Add entries + update assert |
| `TestLoadAllDefaultsPresent` | `config` | 18 classes missing from loaded map | Add ClassFreqConfigs entries |
| `TestLoadNoConfig` | `config` | len==14 assert, now 35 | Update count |
| `TestLoadUnknownClass` | `config` | len==14 assert | Update count |
| `TestLoadNoConfigReturnsLoadResult` | `config` | len==14 assert | Update count |
| `TestPrintConfigContainsAllClasses` | `config` | Hardcoded 14 class names | Expand to all 35 |
| `TestAllClassesCount` | `classify` | want 32, still passes (no new classes yet) | Update to 35 after D-02 |
### Phase Requirements to Test Map
| Req ID | Behavior | Test Type | Automated Command | Test Exists? |
|--------|----------|-----------|-------------------|-------------|
| GRP-02 | PrintConfig shows group headers | unit | `go test ./config/... -run TestPrintConfig` | Partial — `TestPrintConfigContainsAllClasses` exists; new `TestPrintConfigGroupHeaders` needed |
| GRP-02 | Each built-in class appears in correct group | unit | `go test ./config/... -run TestPrintConfigGroupHeaders` | No — Wave 0 |
| GRP-03 | [groups] TOML reassigns class to different group | unit | `go test ./config/... -run TestLoadGroupOverride` | No — Wave 0 |
| GRP-03 | Unknown class in [groups] produces warning | unit | `go test ./config/... -run TestLoadGroupUnknownClass` | No — Wave 0 |
| GRP-03 | PrintConfig reflects group reassignment | unit | `go test ./config/... -run TestPrintConfigGroupReassignment` | No — Wave 0 |
### Sampling Rate
- **Per task commit:** `go test ./classify/... ./synth/... ./config/...`
- **Per wave merge:** `go test ./...`
- **Phase gate:** `go test ./...` fully green before `/gsd:verify-work`
### Wave 0 Gaps
- [ ] `config/config_test.go` — add `TestPrintConfigGroupHeaders` (asserts `# Infrastructure`, `# Mail`, etc. appear in output in correct order)
- [ ] `config/config_test.go` — add `TestLoadGroupOverride` (TOML `[groups]\nIMAP = "Web"` → FreqCfgs[IMAP].Group == "Web")
- [ ] `config/config_test.go` — add `TestLoadGroupUnknownClass` (TOML `[groups]\nBOGUS = "Web"` → no error, warning to stderr)
- [ ] `config/config_test.go` — add `TestPrintConfigGroupReassignment` (LoadResult with group-reassigned IMAP shows IMAP under `# Web` not `# Mail`)
- [ ] `config/config_test.go` — update `TestPrintConfigContainsAllClasses` classNames slice to cover all 35 (or replace with AllClasses() loop)
---
## Open Questions
1. **Pan positions for LDAP, Kerberos, Syslog (Claude's discretion)**
- What we know: Infrastructure family uses [-0.3, +0.3] pan range for existing members (ICMP=-0.3, NTP=-0.1, DHCP=+0.1, DNS=0.0, mDNS=+0.3, SSDP=-0.2, SNMP=+0.2)
- Recommendation: LDAP=-0.2, Kerberos=0.0, Syslog=+0.2 — fills the same spread pattern without exact collision with existing values
2. **TestNewBankHas14Layers function name**
- What we know: The CONTEXT.md calls out that the name is stale (should reflect 35 classes)
- Recommendation: Rename to `TestNewBankHasAllLayers` and update the assertion to `len(b.layers) != len(classify.AllClasses())` — this future-proofs it against further class additions
3. **Discovery group — no existing built-in entries in AllClasses() have Group="Discovery"**
- What we know: mDNS, SSDP, SNMP are currently in AllClasses() but not in ClassFreqConfigs (they are 3 of the 18 missing entries); they belong to "Infrastructure" per the frequency allocation table (slots 3-5, Group=Infrastructure)
- Clarification needed: the frequency table marks them as Infrastructure, but the canonical group order in D-04 lists "Discovery" as a separate group. The table must take precedence — mDNS/SSDP/SNMP are Group="Infrastructure", not "Discovery". The "Discovery" slot in groupOrder may remain empty or be omitted from PrintConfig output (the `if !ok || len(classes) == 0 { continue }` guard handles this).
---
## Sources
### Primary (HIGH confidence)
- Direct code inspection of `/home/dev/workspace/yoloyolo/synth/config.go` — frequency allocation table lines 74-110 (authoritative Hz, waveform, pan, group values)
- Direct code inspection of `/home/dev/workspace/yoloyolo/config/config.go` — PrintConfig, rawConfig struct, merge(), autoAssignFreq(), addAutoFreqEntries()
- Direct code inspection of `/home/dev/workspace/yoloyolo/classify/types.go` — AllClasses(), TrafficClass constants
- Computed autoAssignFreq values by running the FNV hash Go code — LDAP=2950, Kerberos=3250, Syslog=3050
- `go test ./...` output — confirmed exact failing tests and error messages
### Secondary (MEDIUM confidence)
- CONTEXT.md decisions D-01 through D-09 — user decisions locked in prior /gsd:discuss-phase session
- STATE.md accumulated decisions — confirms Phase 10 left AllClasses() at 32 intentionally pending Phase 11
---
## Metadata
**Confidence breakdown:**
- Standard stack: HIGH — no new dependencies; entire phase is code within existing packages
- Architecture: HIGH — patterns derived directly from existing code and locked CONTEXT.md decisions
- Pitfalls: HIGH — derived from actual failing test output and code inspection
- autoAssignFreq Hz values: HIGH — computed by executing the actual FNV hash code
**Research date:** 2026-03-27
**Valid until:** 2026-04-27 (stable — no external packages change)
@@ -0,0 +1,73 @@
---
phase: 11
slug: synthesis-and-config-layer
status: draft
nyquist_compliant: false
wave_0_complete: false
created: 2026-03-27
---
# Phase 11 — Validation Strategy
> Per-phase validation contract for feedback sampling during execution.
---
## Test Infrastructure
| Property | Value |
|----------|-------|
| **Framework** | go test |
| **Config file** | none — standard Go test runner |
| **Quick run command** | `go test ./synth/... ./config/...` |
| **Full suite command** | `go test ./...` |
| **Estimated runtime** | ~10 seconds |
---
## Sampling Rate
- **After every task commit:** Run `go test ./synth/... ./config/...`
- **After every plan wave:** Run `go test ./...`
- **Before `/gsd:verify-work`:** Full suite must be green
- **Max feedback latency:** 10 seconds
---
## Per-Task Verification Map
| Task ID | Plan | Wave | Requirement | Test Type | Automated Command | File Exists | Status |
|---------|------|------|-------------|-----------|-------------------|-------------|--------|
| 11-01-01 | 01 | 1 | GRP-02 | unit | `go test ./synth/... -run TestAllClassesHaveConfig` | ✅ | ⬜ pending |
| 11-01-02 | 01 | 1 | GRP-02 | unit | `go test ./synth/... -run TestFrequenciesUnique` | ✅ | ⬜ pending |
| 11-02-01 | 02 | 2 | GRP-02 | unit | `go test ./config/... -run TestPrintConfig` | ✅ | ⬜ pending |
| 11-02-02 | 02 | 2 | GRP-03 | unit | `go test ./config/... -run TestLoad` | ✅ | ⬜ pending |
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
---
## Wave 0 Requirements
*Existing infrastructure covers all phase requirements. Test helpers already exist in synth and config packages.*
---
## Manual-Only Verifications
| Behavior | Requirement | Why Manual | Test Instructions |
|----------|-------------|------------|-------------------|
| Family-coherent sound output | Success criteria 2 | Perceptual audio quality | Generate MP3 from representative pcap, listen for family grouping |
---
## Validation Sign-Off
- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
- [ ] Wave 0 covers all MISSING references
- [ ] No watch-mode flags
- [ ] Feedback latency < 10s
- [ ] `nyquist_compliant: true` set in frontmatter
**Approval:** pending
@@ -0,0 +1,124 @@
---
phase: 11-synthesis-and-config-layer
verified: 2026-03-27T15:41:00Z
status: human_needed
score: 11/11 must-haves verified
human_verification:
- test: "Run `go build -o /tmp/netsynth . && /tmp/netsynth --print-config | head -5` and listen to an output MP3 generated with mixed traffic types"
expected: "Each traffic class group (Infrastructure, Web, Mail, etc.) produces a perceptually distinct timbre — Triangle waveforms sound softer/bell-like vs Square waveforms (buzzy/hollow) vs Sawtooth (bright/reedy) vs Sine (pure tone)"
why_human: "Perceptual audio quality and family coherence ('distinct, family-coherent sound') requires a human listening test; automated tests only verify Hz/waveform parameters and group metadata, not the sonic result"
---
# Phase 11: Synthesis and Config Layer Verification Report
**Phase Goal:** Every new traffic class produces a distinct, family-coherent sound and --print-config shows all classes organized by group with section headers
**Verified:** 2026-03-27T15:41:00Z
**Status:** human_needed (all automated checks pass; perceptual audio quality requires human listening test)
**Re-verification:** No — initial verification
## Goal Achievement
### Observable Truths
| # | Truth | Status | Evidence |
|---|-------|--------|---------|
| 1 | ClassFreqConfigs has exactly 35 entries matching AllClasses() | VERIFIED | `grep -c "classify\.Class" synth/config.go` = 35; `--print-config` emits 35 `[sounds.*]` entries; TestAllClassesCount passes with `want 35` |
| 2 | Every new class has correct Hz, waveform, pan, and group from frequency allocation table | VERIFIED | All 21 new entries confirmed in synth/config.go (lines 168-369) with exact Hz, WaveformType, Pan, Group values matching the plan spec; `go test ./synth/...` passes |
| 3 | LDAP, Kerberos, Syslog appear in AllClasses() and have Infrastructure group with Triangle waveform | VERIFIED | classify/types.go line 69: `ClassLDAP, ClassKerberos, ClassSyslog` in AllClasses(); synth/config.go lines 349-369: all three have `WaveformType: WaveformTriangle` and `Group: "Infrastructure"` |
| 4 | go test ./synth/... ./classify/... ./config/... all pass | VERIFIED | `go test ./...` all 7 packages pass (confirmed by fresh run) |
| 5 | PrintConfig output groups classes by family with section header comments | VERIFIED | `--print-config` binary output shows `# Infrastructure`, `# Web`, `# Mail`, `# Remote Access`, `# File Transfer`, `# Database`, `# VoIP`, `# Unknown` headers in canonical order |
| 6 | Groups appear in canonical order: Infrastructure, Web, Mail, Remote Access, File Transfer, Database, Discovery, VoIP, Unknown | VERIFIED | TestPrintConfigGroupHeaders passes; binary spot-check confirms order matches groupOrder slice in config.go line 284 |
| 7 | Within each group, classes are sorted by ascending BaseHz | VERIFIED | PrintConfig spot-check output shows ascending Hz within each group (e.g., Infrastructure: 65, 73, 82, 93, 105, 118, 133, 2950, 3050, 3250); sort.Slice by BaseHz confirmed in config.go lines 334-339 |
| 8 | User-defined classes appear under a User-defined section header after all built-in groups | VERIFIED | config.go lines 399-419 emit `# User-defined` section; `# User-defined` string present in source |
| 9 | Users can define [groups] in TOML to reassign a class to a different group | VERIFIED | TestLoadGroupOverride and TestPrintConfigGroupReassignment both pass; applyGroupOverrides called in Load() at line 103 after merge() |
| 10 | Unknown class names in [groups] produce a warning, not an error | VERIFIED | TestLoadGroupUnknownClass passes; test output shows `Warning: config: [groups]: unknown class "BOGUS" (ignored)` on stderr |
| 11 | Group reassignment only affects PrintConfig grouping, not frequency or waveform | VERIFIED | TestLoadGroupOverride asserts `cfg.BaseHz == defaultCfg.BaseHz` (unchanged); applyGroupOverrides only mutates `cfg.Group`, not BaseHz or WaveformType |
**Score:** 11/11 truths verified (automated); 1 truth requires human verification (perceptual audio quality)
### Required Artifacts
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `synth/config.go` | 21 new ClassFreqConfigs entries | VERIFIED | 35 total entries; all 21 new classes present with correct values per allocation table |
| `classify/types.go` | AllClasses() returns 35 entries including LDAP/Kerberos/Syslog | VERIFIED | 35-entry return slice; LDAP/Kerberos/Syslog added to Infrastructure section at line 69 |
| `config/config.go` | Group-ordered PrintConfig, [groups] TOML support, applyGroupOverrides function | VERIFIED | groupOrder slice (line 284), applyGroupOverrides function (line 270), rawConfig.Groups field (line 41), called in Load() (line 103) |
| `config/config_test.go` | Tests for group headers, group reassignment, unknown class warning | VERIFIED | 4 new test functions at lines 605-692: TestPrintConfigGroupHeaders, TestLoadGroupOverride, TestLoadGroupUnknownClass, TestPrintConfigGroupReassignment — all pass |
### Key Link Verification
| From | To | Via | Status | Details |
|------|----|-----|--------|---------|
| `config/config.go` | `synth/config.go` | PrintConfig reads FreqConfig.Group field | WIRED | `cfg.Group` used at lines 329, 364; FreqConfig.Group drives group bucketing in builtinByGroup map |
| `config/config.go` | `classify/types.go` | PrintConfig iterates AllClasses() and groups by Group field | WIRED | `classify.AllClasses()` called at lines 326 and 362; iteration drives builtinByGroup construction and customGroups scan |
| `synth/config.go` | `classify/types.go` | ClassFreqConfigs references TrafficClass constants | WIRED | All 35 map keys use `classify.Class*` constants; pattern `classify\.Class(IMAP|POP3|...)` confirmed present |
### Data-Flow Trace (Level 4)
| Artifact | Data Variable | Source | Produces Real Data | Status |
|----------|---------------|--------|--------------------|--------|
| `config/config.go PrintConfig` | `builtinByGroup` | `classify.AllClasses()` + `result.FreqCfgs[cls]` | Yes — 35 real entries from copyDefaults() which copies synth.ClassFreqConfigs | FLOWING |
| `config/config.go PrintConfig` | `result.FreqCfgs` | `Load()``copyDefaults()``merge()``applyGroupOverrides()` | Yes — real map with 35 entries, plus any user overrides applied | FLOWING |
### Behavioral Spot-Checks
| Behavior | Command | Result | Status |
|----------|---------|--------|--------|
| --print-config emits group headers | `/tmp/netsynth_test --print-config \| grep "^# [A-Z]"` | Infrastructure, Web, Mail, Remote Access, File Transfer, Database, VoIP, Unknown all present | PASS |
| 35 sound entries emitted | `/tmp/netsynth_test --print-config \| grep "^\[sounds\." \| wc -l` | 35 | PASS |
| Classes sorted ascending within group | Infrastructure group: 65, 73, 82, 93, 105, 118, 133, 2950, 3050, 3250 Hz | Correct ascending order | PASS |
| go test ./... passes all packages | `go test ./...` | 7 packages ok | PASS |
| Perceptual audio quality | Requires listening to generated MP3 | Cannot verify programmatically | SKIP (human_needed) |
### Requirements Coverage
| Requirement | Source Plan | Description | Status | Evidence |
|-------------|------------|-------------|--------|---------|
| GRP-02 | 11-01-PLAN.md, 11-02-PLAN.md | `--print-config` output organizes classes by group with section headers | SATISFIED | TestPrintConfigGroupHeaders passes; binary spot-check confirms group headers in canonical order; config.go groupOrder drives emission order |
| GRP-03 | 11-01-PLAN.md, 11-02-PLAN.md | Users can define `[groups]` in TOML config to reassign protocols to different sound families | SATISFIED | rawConfig.Groups field parses `[groups]` TOML; applyGroupOverrides wired into Load(); TestLoadGroupOverride and TestPrintConfigGroupReassignment both pass |
No orphaned requirements — both GRP-02 and GRP-03 are claimed by both plans and satisfied by implementation.
### Anti-Patterns Found
| File | Line | Pattern | Severity | Impact |
|------|------|---------|----------|--------|
| None found | — | — | — | — |
No TODO/FIXME/placeholder comments, no empty handlers, no stub return values in phase-modified files. The `return []HarmonicDef{{Ratio: 1, Amplitude: 1.0}}` in WaveformSine case is the legitimate single-harmonic definition, not a stub.
Hardcoded count audit:
- `grep "want 14" config/config_test.go` — no matches
- `grep "want 32" classify/classifier_test.go` — no matches
- `grep "!= 14" synth/bank_test.go` — no matches
- TestNewBankHas14Layers renamed to TestNewBankHasAllLayers
### Human Verification Required
#### 1. Perceptual Audio Quality — Family-Coherent Distinct Sounds
**Test:** Build the binary (`go build -o /tmp/netsynth .`), run it against a live interface or a pcap file, and listen to the output MP3. Alternatively, generate a test clip exercising multiple classes using a synthetic pcap.
**Expected:** Traffic classes within the same family share a recognizable waveform timbre:
- Infrastructure classes (ICMP, DNS, DHCP, LDAP, etc.) — gentle bell-like quality from Triangle waveform
- Remote Access and File Transfer classes (SSH, RDP, FTP, SMB, etc.) — buzzy/hollow timbre from Square waveform
- Database classes (MySQL, PostgreSQL, Redis, MongoDB) — bright/reedy timbre from Sawtooth waveform
- SIP (VoIP) — pure clean tone from Sine waveform
- Different families should sound clearly distinct from one another while classes within a family are recognizably related
**Why human:** Perceptual audio quality (timbre coherence, distinctness under real traffic loads, absence of clashing frequencies) cannot be verified programmatically. The code correctly implements the waveform types and Hz values, but whether the resulting sound is perceptually "family-coherent" as described in the phase goal requires a human ears-on test.
### Gaps Summary
No gaps. All automated must-haves pass. The only pending item is a human listening test for perceptual audio quality (Success Criterion 2 from the phase scope, flagged at verification request time).
**Commit trail verified:**
- `1556ce8` — feat(11-01): add 21 ClassFreqConfigs entries and include LDAP/Kerberos/Syslog in AllClasses()
- `35d8915` — fix(11-01): update hardcoded count assertions to reflect 35 traffic classes
- `374282e` — feat(11-02): add [groups] TOML support and group-ordered PrintConfig
- `7bf3ea1` — test(11-02): add group header and reassignment tests (GRP-02, GRP-03)
---
_Verified: 2026-03-27T15:41:00Z_
_Verifier: Claude (gsd-verifier)_