docs(11): create phase plan
This commit is contained in:
@@ -91,7 +91,10 @@ Plans:
|
||||
2. A pcap or live capture that triggers Mail traffic produces tones that are audibly in the same timbral family — same waveform type, similar frequency register — while still being distinguishable from each other
|
||||
3. Users can define `[groups]` in their TOML config to reassign a protocol to a different sound family, and --print-config reflects the reassignment
|
||||
4. `go test ./...` passes and a listening test on a representative pcap confirms family identity is perceptually clear
|
||||
**Plans**: TBD
|
||||
**Plans:** 2 plans
|
||||
Plans:
|
||||
- [ ] 11-01-PLAN.md — Add 21 ClassFreqConfigs entries, update AllClasses() to 35, fix all count tests
|
||||
- [ ] 11-02-PLAN.md — Refactor PrintConfig for group headers, add [groups] TOML support
|
||||
|
||||
## Progress
|
||||
|
||||
@@ -107,4 +110,4 @@ Plans:
|
||||
| 8. Test and Constant Cleanup | v1.2 | 1/1 | Complete | 2026-03-27 |
|
||||
| 9. Frequency Design and Group Architecture | v1.2 | 1/2 | In Progress| |
|
||||
| 10. Classification Layer | v1.2 | 2/2 | Complete | 2026-03-27 |
|
||||
| 11. Synthesis and Config Layer | v1.2 | 0/? | Not started | - |
|
||||
| 11. Synthesis and Config Layer | v1.2 | 0/2 | In Progress | - |
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
---
|
||||
phase: 11-synthesis-and-config-layer
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- synth/config.go
|
||||
- classify/types.go
|
||||
- synth/bank_test.go
|
||||
- config/config_test.go
|
||||
- classify/classifier_test.go
|
||||
autonomous: true
|
||||
requirements:
|
||||
- GRP-02
|
||||
- GRP-03
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "ClassFreqConfigs has exactly 35 entries matching AllClasses()"
|
||||
- "Every new class has correct Hz, waveform, pan, and group from frequency allocation table"
|
||||
- "LDAP, Kerberos, Syslog appear in AllClasses() and have Infrastructure group with Triangle waveform"
|
||||
- "go test ./synth/... ./classify/... ./config/... all pass"
|
||||
artifacts:
|
||||
- path: "synth/config.go"
|
||||
provides: "21 new ClassFreqConfigs entries"
|
||||
contains: "classify.ClassIMAP"
|
||||
- path: "classify/types.go"
|
||||
provides: "AllClasses() returns 35 entries including LDAP/Kerberos/Syslog"
|
||||
contains: "ClassLDAP"
|
||||
key_links:
|
||||
- from: "synth/config.go"
|
||||
to: "classify/types.go"
|
||||
via: "ClassFreqConfigs references TrafficClass constants"
|
||||
pattern: "classify\\.Class(IMAP|POP3|SMTPSub|RDP|Telnet|VNC|FTP|SMB|TFTP|MySQL|PostgreSQL|Redis|MongoDB|SIP|QUIC|MDNS|SSDP|SNMP|LDAP|Kerberos|Syslog)"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Add all 21 missing ClassFreqConfigs entries and update AllClasses() to include LDAP/Kerberos/Syslog, then fix every hardcoded count assertion across synth, config, and classify test files.
|
||||
|
||||
Purpose: This is the foundational data layer for Phase 11 -- all subsequent work (PrintConfig group headers, TOML [groups]) depends on all 35 classes having complete synthesis configs.
|
||||
Output: synth/config.go with 35 ClassFreqConfigs entries, classify/types.go with 35-entry AllClasses(), all count-based tests green.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@synth/config.go
|
||||
@classify/types.go
|
||||
@synth/bank_test.go
|
||||
@synth/config_test.go
|
||||
@config/config_test.go
|
||||
@classify/classifier_test.go
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types and contracts the executor needs -->
|
||||
|
||||
From classify/types.go:
|
||||
```go
|
||||
type TrafficClass string
|
||||
// Constants: ClassIMAP, ClassPOP3, ClassSMTPSub, ClassFTP, ClassSMB, ClassTFTP,
|
||||
// ClassRDP, ClassTelnet, ClassVNC, ClassMySQL, ClassPostgreSQL, ClassRedis,
|
||||
// ClassMongoDB, ClassMDNS, ClassSSDP, ClassSNMP, ClassSIP, ClassQUIC,
|
||||
// ClassLDAP, ClassKerberos, ClassSyslog
|
||||
func AllClasses() []TrafficClass
|
||||
```
|
||||
|
||||
From synth/config.go:
|
||||
```go
|
||||
type FreqConfig struct {
|
||||
BaseHz float64
|
||||
Harmonics []HarmonicDef
|
||||
Pan float64
|
||||
WaveformType WaveformType
|
||||
Group string
|
||||
}
|
||||
var ClassFreqConfigs = map[classify.TrafficClass]FreqConfig{...}
|
||||
func WaveformPresetHarmonics(wt WaveformType, baseHz float64, sampleRate int) []HarmonicDef
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Add 21 ClassFreqConfigs entries and update AllClasses()</name>
|
||||
<files>synth/config.go, classify/types.go</files>
|
||||
<read_first>synth/config.go, classify/types.go</read_first>
|
||||
<action>
|
||||
**synth/config.go** -- Add 21 new entries to the ClassFreqConfigs map, after the existing entries and before the closing brace. Use WaveformPresetHarmonics() for all entries (per D-01). The exact values from the frequency allocation table (lines 74-110 of synth/config.go):
|
||||
|
||||
```
|
||||
// --- Infrastructure additions (Triangle, 93-118 Hz) ---
|
||||
classify.ClassMDNS: {BaseHz: 93.0, WaveformType: WaveformTriangle, Harmonics: WaveformPresetHarmonics(WaveformTriangle, 93.0, SampleRate), Pan: 0.3, Group: "Infrastructure"}
|
||||
classify.ClassSSDP: {BaseHz: 105.0, WaveformType: WaveformTriangle, Harmonics: WaveformPresetHarmonics(WaveformTriangle, 105.0, SampleRate), Pan: -0.2, Group: "Infrastructure"}
|
||||
classify.ClassSNMP: {BaseHz: 118.0, WaveformType: WaveformTriangle, Harmonics: WaveformPresetHarmonics(WaveformTriangle, 118.0, SampleRate), Pan: 0.2, Group: "Infrastructure"}
|
||||
// --- Web addition (Sawtooth, 190 Hz) ---
|
||||
classify.ClassQUIC: {BaseHz: 190.0, WaveformType: WaveformSawtooth, Harmonics: WaveformPresetHarmonics(WaveformSawtooth, 190.0, SampleRate), Pan: -0.2, Group: "Web"}
|
||||
// --- Mail additions (Triangle, 241-305 Hz) ---
|
||||
classify.ClassIMAP: {BaseHz: 241.0, WaveformType: WaveformTriangle, Harmonics: WaveformPresetHarmonics(WaveformTriangle, 241.0, SampleRate), Pan: 0.3, Group: "Mail"}
|
||||
classify.ClassPOP3: {BaseHz: 271.0, WaveformType: WaveformTriangle, Harmonics: WaveformPresetHarmonics(WaveformTriangle, 271.0, SampleRate), Pan: 0.4, Group: "Mail"}
|
||||
classify.ClassSMTPSub: {BaseHz: 305.0, WaveformType: WaveformTriangle, Harmonics: WaveformPresetHarmonics(WaveformTriangle, 305.0, SampleRate), Pan: 0.5, Group: "Mail"}
|
||||
// --- Remote Access additions (Square, 385-485 Hz) ---
|
||||
classify.ClassRDP: {BaseHz: 385.0, WaveformType: WaveformSquare, Harmonics: WaveformPresetHarmonics(WaveformSquare, 385.0, SampleRate), Pan: -0.6, Group: "Remote Access"}
|
||||
classify.ClassTelnet: {BaseHz: 432.0, WaveformType: WaveformSquare, Harmonics: WaveformPresetHarmonics(WaveformSquare, 432.0, SampleRate), Pan: -0.5, Group: "Remote Access"}
|
||||
classify.ClassVNC: {BaseHz: 485.0, WaveformType: WaveformSquare, Harmonics: WaveformPresetHarmonics(WaveformSquare, 485.0, SampleRate), Pan: -0.4, Group: "Remote Access"}
|
||||
// --- File Transfer additions (Square, 545-687 Hz) ---
|
||||
classify.ClassFTP: {BaseHz: 545.0, WaveformType: WaveformSquare, Harmonics: WaveformPresetHarmonics(WaveformSquare, 545.0, SampleRate), Pan: 0.5, Group: "File Transfer"}
|
||||
classify.ClassSMB: {BaseHz: 612.0, WaveformType: WaveformSquare, Harmonics: WaveformPresetHarmonics(WaveformSquare, 612.0, SampleRate), Pan: 0.6, Group: "File Transfer"}
|
||||
classify.ClassTFTP: {BaseHz: 687.0, WaveformType: WaveformSquare, Harmonics: WaveformPresetHarmonics(WaveformSquare, 687.0, SampleRate), Pan: 0.7, Group: "File Transfer"}
|
||||
// --- Database additions (Sawtooth, 1543-2182 Hz) ---
|
||||
classify.ClassMySQL: {BaseHz: 1543.0, WaveformType: WaveformSawtooth, Harmonics: WaveformPresetHarmonics(WaveformSawtooth, 1543.0, SampleRate), Pan: -0.4, Group: "Database"}
|
||||
classify.ClassPostgreSQL: {BaseHz: 1732.0, WaveformType: WaveformSawtooth, Harmonics: WaveformPresetHarmonics(WaveformSawtooth, 1732.0, SampleRate), Pan: -0.2, Group: "Database"}
|
||||
classify.ClassRedis: {BaseHz: 1944.0, WaveformType: WaveformSawtooth, Harmonics: WaveformPresetHarmonics(WaveformSawtooth, 1944.0, SampleRate), Pan: 0.2, Group: "Database"}
|
||||
classify.ClassMongoDB: {BaseHz: 2182.0, WaveformType: WaveformSawtooth, Harmonics: WaveformPresetHarmonics(WaveformSawtooth, 2182.0, SampleRate), Pan: 0.4, Group: "Database"}
|
||||
// --- VoIP (Sine, 2449 Hz) ---
|
||||
classify.ClassSIP: {BaseHz: 2449.0, WaveformType: WaveformSine, Harmonics: WaveformPresetHarmonics(WaveformSine, 2449.0, SampleRate), Pan: 0.0, Group: "VoIP"}
|
||||
// --- Infrastructure auto-assigned (Triangle, 2950-3250 Hz) per D-02 ---
|
||||
classify.ClassLDAP: {BaseHz: 2950.0, WaveformType: WaveformTriangle, Harmonics: WaveformPresetHarmonics(WaveformTriangle, 2950.0, SampleRate), Pan: -0.2, Group: "Infrastructure"}
|
||||
classify.ClassKerberos: {BaseHz: 3250.0, WaveformType: WaveformTriangle, Harmonics: WaveformPresetHarmonics(WaveformTriangle, 3250.0, SampleRate), Pan: 0.0, Group: "Infrastructure"}
|
||||
classify.ClassSyslog: {BaseHz: 3050.0, WaveformType: WaveformTriangle, Harmonics: WaveformPresetHarmonics(WaveformTriangle, 3050.0, SampleRate), Pan: 0.2, Group: "Infrastructure"}
|
||||
```
|
||||
|
||||
Place new entries in the map grouped by family with section comments matching the existing pattern (e.g., `// --- Infrastructure additions ...`). Insert them logically:
|
||||
- Infrastructure additions (mDNS, SSDP, SNMP) after ClassDHCP and before ClassDNS (since 93/105/118 Hz come between DHCP=82 and DNS=133)
|
||||
- Web addition (QUIC) after ClassHTTP
|
||||
- Mail additions after ClassSMTP
|
||||
- Remote Access additions after ClassSSH
|
||||
- File Transfer after Unknown entries
|
||||
- Database after Unknown entries
|
||||
- VoIP after Database
|
||||
- LDAP/Kerberos/Syslog at end (auto-assigned range)
|
||||
|
||||
**classify/types.go** -- Per D-02 and D-03:
|
||||
1. Add ClassLDAP, ClassKerberos, ClassSyslog to AllClasses() in the Infrastructure section, after ClassSNMP.
|
||||
2. Remove the comment "Excludes ClassLDAP, ClassKerberos, and ClassSyslog" from the AllClasses() doc comment.
|
||||
3. Update the doc comment to say "AllClasses returns all known traffic classes in display order."
|
||||
4. The AllClasses() function should now return 35 entries total.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/dev/workspace/yoloyolo && go build ./synth/... ./classify/...</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -c "classify\.Class" synth/config.go` shows at least 35 occurrences in ClassFreqConfigs
|
||||
- `grep "ClassLDAP" classify/types.go` appears in AllClasses() return slice
|
||||
- `grep "ClassKerberos" classify/types.go` appears in AllClasses() return slice
|
||||
- `grep "ClassSyslog" classify/types.go` appears in AllClasses() return slice
|
||||
- `go build ./synth/... ./classify/...` succeeds
|
||||
</acceptance_criteria>
|
||||
<done>ClassFreqConfigs has 35 entries with correct Hz/waveform/pan/group values. AllClasses() returns 35 entries including LDAP/Kerberos/Syslog. Both packages compile.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Fix all hardcoded count assertions in tests</name>
|
||||
<files>synth/bank_test.go, config/config_test.go, classify/classifier_test.go</files>
|
||||
<read_first>synth/bank_test.go, config/config_test.go, classify/classifier_test.go</read_first>
|
||||
<action>
|
||||
Update all hardcoded count assertions to reflect the new 35-entry state. Per research Pitfall 1 and Pitfall 2:
|
||||
|
||||
**synth/bank_test.go:**
|
||||
- Line 10: Rename `TestNewBankHas14Layers` to `TestNewBankHasAllLayers`
|
||||
- Line 12: Change `len(b.layers) != 14` to `len(b.layers) != len(classify.AllClasses())`
|
||||
- Line 13: Change `want 14` to a dynamic message using `len(classify.AllClasses())`
|
||||
|
||||
**config/config_test.go:**
|
||||
- Line 120: Change `len(cfgs) != 14` to `len(cfgs) != len(classify.AllClasses())` in TestLoadNoConfig
|
||||
- Line 149: Change `len(cfgs) != 14` to `len(cfgs) != len(classify.AllClasses())` in TestLoadUnknownClass
|
||||
- Line 180: Change `len(cfgs) != 14` to `len(cfgs) != len(classify.AllClasses())` in TestLoadAllDefaultsPresent
|
||||
- Line 617: Change `len(result.FreqCfgs) != 14` to `len(result.FreqCfgs) != len(classify.AllClasses())` in TestLoadNoConfigReturnsLoadResult
|
||||
- Lines 407-416 in TestPrintConfigContainsAllClasses: Replace the hardcoded `classNames` slice with a loop over `classify.AllClasses()`. Change to:
|
||||
```go
|
||||
for _, cls := range classify.AllClasses() {
|
||||
if !strings.Contains(output, string(cls)) {
|
||||
t.Errorf("PrintConfig output missing class %q", cls)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**classify/classifier_test.go:**
|
||||
- Line 459 (approximately): Change `want 32` to `want 35` in TestAllClassesCount. Update the assertion value from 32 to 35.
|
||||
|
||||
Use `len(classify.AllClasses())` for dynamic counts wherever possible (synth and config tests). For classifier_test.go, use the literal 35 since the test is specifically verifying the count is a known value per D-03.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/dev/workspace/yoloyolo && go test ./synth/... ./classify/... ./config/...</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `go test ./synth/...` passes (0 failures)
|
||||
- `go test ./classify/...` passes (0 failures)
|
||||
- `go test ./config/...` passes (0 failures)
|
||||
- `grep "14" synth/bank_test.go` returns no lines with hardcoded layer counts
|
||||
- `grep 'want 14' config/config_test.go` returns no matches
|
||||
- `grep 'want 32' classify/classifier_test.go` returns no matches
|
||||
</acceptance_criteria>
|
||||
<done>All test suites pass with 35 classes. No hardcoded counts of 14 or 32 remain in test assertions. TestNewBankHas14Layers renamed to TestNewBankHasAllLayers.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
```bash
|
||||
cd /home/dev/workspace/yoloyolo && go test ./...
|
||||
```
|
||||
All tests pass. ClassFreqConfigs has 35 entries matching AllClasses().
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- `go test ./...` passes fully
|
||||
- ClassFreqConfigs map has exactly 35 entries
|
||||
- AllClasses() returns exactly 35 entries
|
||||
- No hardcoded counts of 14 or 32 remain in test files
|
||||
- Every new entry uses WaveformPresetHarmonics() (not hand-tuned harmonics)
|
||||
- LDAP/Kerberos/Syslog have Group="Infrastructure" and WaveformTriangle
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/11-synthesis-and-config-layer/11-01-SUMMARY.md`
|
||||
</output>
|
||||
@@ -0,0 +1,415 @@
|
||||
---
|
||||
phase: 11-synthesis-and-config-layer
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["11-01"]
|
||||
files_modified:
|
||||
- config/config.go
|
||||
- config/config_test.go
|
||||
autonomous: true
|
||||
requirements:
|
||||
- GRP-02
|
||||
- GRP-03
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "PrintConfig output groups classes by family with section header comments"
|
||||
- "Groups appear in canonical order: Infrastructure, Web, Mail, Remote Access, File Transfer, Database, Discovery, VoIP, Unknown"
|
||||
- "Within each group, classes are sorted by ascending BaseHz"
|
||||
- "User-defined classes appear under a User-defined section header after all built-in groups"
|
||||
- "Users can define [groups] in TOML to reassign a class to a different group"
|
||||
- "Unknown class names in [groups] produce a warning, not an error"
|
||||
- "Group reassignment only affects PrintConfig grouping, not frequency or waveform"
|
||||
artifacts:
|
||||
- path: "config/config.go"
|
||||
provides: "Group-ordered PrintConfig, [groups] TOML support, applyGroupOverrides function"
|
||||
exports: ["PrintConfig", "Load", "LoadResult"]
|
||||
- path: "config/config_test.go"
|
||||
provides: "Tests for group headers, group reassignment, unknown class warning"
|
||||
contains: "TestPrintConfigGroupHeaders"
|
||||
key_links:
|
||||
- from: "config/config.go"
|
||||
to: "synth/config.go"
|
||||
via: "PrintConfig reads FreqConfig.Group field"
|
||||
pattern: "cfg\\.Group"
|
||||
- from: "config/config.go"
|
||||
to: "classify/types.go"
|
||||
via: "PrintConfig iterates AllClasses() and groups by Group field"
|
||||
pattern: "classify\\.AllClasses"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Refactor PrintConfig to group classes by their Group field with section headers (GRP-02), and add [groups] TOML config support for reassigning protocols to different sound families (GRP-03).
|
||||
|
||||
Purpose: This is the user-facing output change that makes --print-config show organized, family-coherent class listings, and gives users the ability to rearrange groupings via TOML config.
|
||||
Output: PrintConfig emits group headers in canonical order; [groups] TOML table parsed and applied; tests cover group headers, reassignment, and unknown class warnings.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/11-synthesis-and-config-layer/11-01-SUMMARY.md
|
||||
@config/config.go
|
||||
@config/config_test.go
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types and contracts from Plan 01 output -->
|
||||
|
||||
From synth/config.go (after Plan 01):
|
||||
```go
|
||||
type FreqConfig struct {
|
||||
BaseHz float64
|
||||
Harmonics []HarmonicDef
|
||||
Pan float64
|
||||
WaveformType WaveformType
|
||||
Group string // "Infrastructure", "Web", "Mail", "Remote Access", "File Transfer", "Database", "Discovery", "VoIP", "Unknown"
|
||||
}
|
||||
var ClassFreqConfigs = map[classify.TrafficClass]FreqConfig{...} // 35 entries
|
||||
```
|
||||
|
||||
From classify/types.go (after Plan 01):
|
||||
```go
|
||||
func AllClasses() []TrafficClass // returns 35 entries including LDAP/Kerberos/Syslog
|
||||
```
|
||||
|
||||
From config/config.go (current):
|
||||
```go
|
||||
type rawConfig struct {
|
||||
Sounds map[string]SoundOverride `toml:"sounds"`
|
||||
Rules []RawRule `toml:"rules"`
|
||||
}
|
||||
type LoadResult struct {
|
||||
FreqCfgs map[classify.TrafficClass]synth.FreqConfig
|
||||
UserRules []classify.Rule
|
||||
ConfigPath string
|
||||
AutoClasses map[classify.TrafficClass]bool
|
||||
}
|
||||
func Load(configPath string) (LoadResult, error)
|
||||
func PrintConfig(result LoadResult) string
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Add [groups] TOML support and refactor PrintConfig for group headers</name>
|
||||
<files>config/config.go</files>
|
||||
<read_first>config/config.go</read_first>
|
||||
<action>
|
||||
Three changes to config/config.go:
|
||||
|
||||
**1. Add Groups field to rawConfig struct (per D-07):**
|
||||
```go
|
||||
type rawConfig struct {
|
||||
Sounds map[string]SoundOverride `toml:"sounds"`
|
||||
Rules []RawRule `toml:"rules"`
|
||||
Groups map[string]string `toml:"groups"`
|
||||
}
|
||||
```
|
||||
|
||||
**2. Add applyGroupOverrides function and wire into Load() (per D-07/D-08/D-09):**
|
||||
|
||||
Add a new function `applyGroupOverrides`:
|
||||
```go
|
||||
// applyGroupOverrides overlays [groups] reassignments onto freqCfgs.Group in-place.
|
||||
// Unknown class names produce a warning to stderr (D-09).
|
||||
// Unknown group names are silently accepted -- users can invent custom groups (D-09).
|
||||
func applyGroupOverrides(cfgs map[classify.TrafficClass]synth.FreqConfig, groups map[string]string) {
|
||||
for className, groupName := range groups {
|
||||
cls := classify.TrafficClass(className)
|
||||
cfg, known := cfgs[cls]
|
||||
if !known {
|
||||
fmt.Fprintf(os.Stderr, "Warning: config: [groups]: unknown class %q (ignored)\n", className)
|
||||
continue
|
||||
}
|
||||
cfg.Group = groupName
|
||||
cfgs[cls] = cfg
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In Load(), after the `merge(freqCfgs, raw.Sounds)` call (around line 101), add:
|
||||
```go
|
||||
applyGroupOverrides(freqCfgs, raw.Groups)
|
||||
```
|
||||
|
||||
This goes AFTER merge so that group reassignment is the last transformation before returning. The call should be present in both code paths (with config file). The no-config path (line 77) does not need it since there is no raw.Groups to apply.
|
||||
|
||||
**3. Refactor PrintConfig for group-ordered output (per D-04/D-05/D-06):**
|
||||
|
||||
Replace the current flat `AllClasses()` iteration (lines 298-321) and the user-defined section (lines 323-333) with group-ordered output.
|
||||
|
||||
Add a package-level variable for canonical group order:
|
||||
```go
|
||||
// groupOrder defines the canonical display order for --print-config section headers (D-04).
|
||||
var groupOrder = []string{
|
||||
"Infrastructure", "Web", "Mail", "Remote Access",
|
||||
"File Transfer", "Database", "Discovery", "VoIP", "Unknown",
|
||||
}
|
||||
```
|
||||
|
||||
Replace the built-in and user-defined emission blocks with:
|
||||
```go
|
||||
// Build group -> []TrafficClass index from AllClasses()
|
||||
builtinByGroup := map[string][]classify.TrafficClass{}
|
||||
builtinSet := map[classify.TrafficClass]bool{}
|
||||
for _, cls := range classify.AllClasses() {
|
||||
builtinSet[cls] = true
|
||||
cfg := result.FreqCfgs[cls]
|
||||
grp := cfg.Group
|
||||
builtinByGroup[grp] = append(builtinByGroup[grp], cls)
|
||||
}
|
||||
|
||||
// Sort each group by ascending BaseHz (D-05) using result.FreqCfgs (effective Hz, not defaults)
|
||||
for grp := range builtinByGroup {
|
||||
classes := builtinByGroup[grp]
|
||||
sort.Slice(classes, func(i, j int) bool {
|
||||
return result.FreqCfgs[classes[i]].BaseHz < result.FreqCfgs[classes[j]].BaseHz
|
||||
})
|
||||
}
|
||||
|
||||
// Emit built-in classes grouped with headers (D-04)
|
||||
for _, grp := range groupOrder {
|
||||
classes, ok := builtinByGroup[grp]
|
||||
if !ok || len(classes) == 0 {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(&sb, "# %s\n\n", grp)
|
||||
for _, cls := range classes {
|
||||
cfg := result.FreqCfgs[cls]
|
||||
annotation := classAnnotation(cls, cfg, result.AutoClasses)
|
||||
fmt.Fprintf(&sb, "# %s -- %.1f Hz (%s)\n", string(cls), cfg.BaseHz, annotation)
|
||||
fmt.Fprintf(&sb, "[sounds.%s]\n", string(cls))
|
||||
fmt.Fprintf(&sb, "frequency = %.1f\n", cfg.BaseHz)
|
||||
fmt.Fprintf(&sb, "waveform = %q\n", waveformString(cfg.WaveformType))
|
||||
fmt.Fprintf(&sb, "\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Check for custom groups (from [groups] reassignment) that are not in groupOrder
|
||||
// These classes were already emitted under their reassigned group if the group is canonical.
|
||||
// For non-canonical group names (user-invented), collect and emit separately.
|
||||
customGroups := map[string][]classify.TrafficClass{}
|
||||
for _, cls := range classify.AllClasses() {
|
||||
cfg := result.FreqCfgs[cls]
|
||||
grp := cfg.Group
|
||||
isCanonical := false
|
||||
for _, cg := range groupOrder {
|
||||
if grp == cg {
|
||||
isCanonical = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isCanonical {
|
||||
customGroups[grp] = append(customGroups[grp], cls)
|
||||
}
|
||||
}
|
||||
// Sort and emit custom group sections
|
||||
var customGroupNames []string
|
||||
for grp := range customGroups {
|
||||
customGroupNames = append(customGroupNames, grp)
|
||||
}
|
||||
sort.Strings(customGroupNames)
|
||||
for _, grp := range customGroupNames {
|
||||
classes := customGroups[grp]
|
||||
sort.Slice(classes, func(i, j int) bool {
|
||||
return result.FreqCfgs[classes[i]].BaseHz < result.FreqCfgs[classes[j]].BaseHz
|
||||
})
|
||||
fmt.Fprintf(&sb, "# %s\n\n", grp)
|
||||
for _, cls := range classes {
|
||||
cfg := result.FreqCfgs[cls]
|
||||
annotation := classAnnotation(cls, cfg, result.AutoClasses)
|
||||
fmt.Fprintf(&sb, "# %s -- %.1f Hz (%s)\n", string(cls), cfg.BaseHz, annotation)
|
||||
fmt.Fprintf(&sb, "[sounds.%s]\n", string(cls))
|
||||
fmt.Fprintf(&sb, "frequency = %.1f\n", cfg.BaseHz)
|
||||
fmt.Fprintf(&sb, "waveform = %q\n", waveformString(cfg.WaveformType))
|
||||
fmt.Fprintf(&sb, "\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Emit user-defined classes (in FreqCfgs but not in AllClasses) under "# User-defined" (D-06)
|
||||
var userClasses []string
|
||||
for cls := range result.FreqCfgs {
|
||||
if !builtinSet[cls] {
|
||||
userClasses = append(userClasses, string(cls))
|
||||
}
|
||||
}
|
||||
sort.Strings(userClasses)
|
||||
if len(userClasses) > 0 {
|
||||
fmt.Fprintf(&sb, "# User-defined\n\n")
|
||||
for _, clsStr := range userClasses {
|
||||
cls := classify.TrafficClass(clsStr)
|
||||
cfg := result.FreqCfgs[cls]
|
||||
annotation := classAnnotation(cls, cfg, result.AutoClasses)
|
||||
fmt.Fprintf(&sb, "# %s -- %.1f Hz (%s)\n", clsStr, cfg.BaseHz, annotation)
|
||||
fmt.Fprintf(&sb, "[sounds.%s]\n", clsStr)
|
||||
fmt.Fprintf(&sb, "frequency = %.1f\n", cfg.BaseHz)
|
||||
fmt.Fprintf(&sb, "waveform = %q\n", waveformString(cfg.WaveformType))
|
||||
fmt.Fprintf(&sb, "\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Remove the old `builtinSet` declaration (line 298) since it is now declared in the new block. Remove the old `userClasses` collection and sort (lines 304-310). Remove the old built-in emission loop (lines 313-321) and user-defined emission loop (lines 323-333).
|
||||
|
||||
Do NOT modify the header section (lines 273-281) or rules section (lines 283-295) -- those stay unchanged.
|
||||
|
||||
Important: the `sort` package is already imported. The `builtinSet` map is now declared inside the new block, so remove the old one.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/dev/workspace/yoloyolo && go build ./config/...</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep "groupOrder" config/config.go` returns the canonical group order slice
|
||||
- `grep "applyGroupOverrides" config/config.go` returns the function definition
|
||||
- `grep 'Groups map\[string\]string' config/config.go` shows the new rawConfig field
|
||||
- `grep "# User-defined" config/config.go` shows the user-defined section header
|
||||
- `go build ./config/...` succeeds
|
||||
</acceptance_criteria>
|
||||
<done>PrintConfig emits group-ordered output with section headers. rawConfig has Groups field. applyGroupOverrides function exists and is called in Load(). Compiles successfully.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Add tests for group headers, group reassignment, and unknown class warning</name>
|
||||
<files>config/config_test.go</files>
|
||||
<read_first>config/config_test.go, config/config.go</read_first>
|
||||
<action>
|
||||
Add 4 new test functions to config/config_test.go:
|
||||
|
||||
**TestPrintConfigGroupHeaders** -- Verifies GRP-02 group header output:
|
||||
```go
|
||||
func TestPrintConfigGroupHeaders(t *testing.T) {
|
||||
t.Chdir(t.TempDir())
|
||||
result, err := config.Load("")
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
output := config.PrintConfig(result)
|
||||
|
||||
// Verify all populated group headers appear
|
||||
expectedGroups := []string{"# Infrastructure", "# Web", "# Mail", "# Remote Access", "# File Transfer", "# Database", "# VoIP", "# Unknown"}
|
||||
for _, header := range expectedGroups {
|
||||
if !strings.Contains(output, header+"\n") {
|
||||
t.Errorf("PrintConfig output missing group header %q", header)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify canonical order: Infrastructure before Web before Mail etc.
|
||||
infraIdx := strings.Index(output, "# Infrastructure\n")
|
||||
webIdx := strings.Index(output, "# Web\n")
|
||||
mailIdx := strings.Index(output, "# Mail\n")
|
||||
remoteIdx := strings.Index(output, "# Remote Access\n")
|
||||
ftIdx := strings.Index(output, "# File Transfer\n")
|
||||
dbIdx := strings.Index(output, "# Database\n")
|
||||
voipIdx := strings.Index(output, "# VoIP\n")
|
||||
unknownIdx := strings.Index(output, "# Unknown\n")
|
||||
|
||||
if infraIdx >= webIdx || webIdx >= mailIdx || mailIdx >= remoteIdx ||
|
||||
remoteIdx >= ftIdx || ftIdx >= dbIdx || dbIdx >= voipIdx || voipIdx >= unknownIdx {
|
||||
t.Errorf("Group headers not in canonical order: infra=%d web=%d mail=%d remote=%d ft=%d db=%d voip=%d unknown=%d",
|
||||
infraIdx, webIdx, mailIdx, remoteIdx, ftIdx, dbIdx, voipIdx, unknownIdx)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**TestLoadGroupOverride** -- Verifies GRP-03 basic reassignment:
|
||||
```go
|
||||
func TestLoadGroupOverride(t *testing.T) {
|
||||
path := writeTOML(t, "[groups]\nIMAP = \"Web\"\n")
|
||||
result, err := config.Load(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
cfg := result.FreqCfgs[classify.ClassIMAP]
|
||||
if cfg.Group != "Web" {
|
||||
t.Errorf("IMAP Group: got %q, want %q", cfg.Group, "Web")
|
||||
}
|
||||
// Frequency and waveform unchanged (D-08)
|
||||
defaultCfg := synth.ClassFreqConfigs[classify.ClassIMAP]
|
||||
if cfg.BaseHz != defaultCfg.BaseHz {
|
||||
t.Errorf("IMAP BaseHz changed: got %v, want %v (should be unchanged by group reassignment)", cfg.BaseHz, defaultCfg.BaseHz)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**TestLoadGroupUnknownClass** -- Verifies D-09 warning for unknown class:
|
||||
```go
|
||||
func TestLoadGroupUnknownClass(t *testing.T) {
|
||||
path := writeTOML(t, "[groups]\nBOGUS = \"Web\"\n")
|
||||
result, err := config.Load(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Load should not error on unknown [groups] class: %v", err)
|
||||
}
|
||||
// Should still have all default classes
|
||||
if len(result.FreqCfgs) != len(classify.AllClasses()) {
|
||||
t.Errorf("FreqCfgs len: got %d, want %d", len(result.FreqCfgs), len(classify.AllClasses()))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**TestPrintConfigGroupReassignment** -- Verifies PrintConfig reflects reassignment:
|
||||
```go
|
||||
func TestPrintConfigGroupReassignment(t *testing.T) {
|
||||
path := writeTOML(t, "[groups]\nIMAP = \"Web\"\n")
|
||||
result, err := config.Load(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
output := config.PrintConfig(result)
|
||||
|
||||
// Find the "# Web" section and check IMAP appears after it
|
||||
webIdx := strings.Index(output, "# Web\n")
|
||||
mailIdx := strings.Index(output, "# Mail\n")
|
||||
imapIdx := strings.Index(output, "[sounds.IMAP]")
|
||||
if imapIdx < webIdx || imapIdx > mailIdx {
|
||||
t.Errorf("IMAP (reassigned to Web) should appear between Web and Mail headers; web=%d imap=%d mail=%d", webIdx, imapIdx, mailIdx)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Also update `TestPrintConfigContainsAllClasses` if it was not already updated in Plan 01 -- it should use `classify.AllClasses()` loop instead of hardcoded class names. (Plan 01 should have done this, but verify and fix if needed.)
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/dev/workspace/yoloyolo && go test ./config/... -v -run "TestPrintConfigGroup|TestLoadGroup"</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `go test ./config/... -run TestPrintConfigGroupHeaders` passes
|
||||
- `go test ./config/... -run TestLoadGroupOverride` passes
|
||||
- `go test ./config/... -run TestLoadGroupUnknownClass` passes
|
||||
- `go test ./config/... -run TestPrintConfigGroupReassignment` passes
|
||||
- `go test ./config/...` all tests pass (no regressions)
|
||||
</acceptance_criteria>
|
||||
<done>Four new tests cover GRP-02 (group headers in canonical order) and GRP-03 (group reassignment, unknown class warning, PrintConfig reflects reassignment). Full config test suite passes.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
```bash
|
||||
cd /home/dev/workspace/yoloyolo && go test ./...
|
||||
```
|
||||
All tests pass. PrintConfig shows group headers. [groups] TOML works for reassignment.
|
||||
|
||||
Manual verification: `go run . --print-config` shows classes organized by group with `# Infrastructure`, `# Web`, `# Mail`, `# Remote Access`, `# File Transfer`, `# Database`, `# VoIP`, `# Unknown` section headers.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- `go test ./...` passes fully
|
||||
- PrintConfig output contains group section headers in canonical order
|
||||
- [groups] TOML table reassigns a class's group in PrintConfig output
|
||||
- Unknown class names in [groups] produce stderr warning, not error
|
||||
- Group reassignment does not change frequency or waveform (D-08)
|
||||
- User-defined classes appear under "# User-defined" section
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/11-synthesis-and-config-layer/11-02-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user