diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index 6bc14de..3d6eabd 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -35,7 +35,7 @@ Full details: `.planning/milestones/v1.1-ROADMAP.md`
**Milestone Goal:** Expand traffic classification with grouped protocol families that share recognizable sound signatures — from 14 classes to ~35, organized into frequency bands by family.
-- [ ] **Phase 8: Test and Constant Cleanup** - Remove stale constants and update test bounds that would block all subsequent v1.2 work
+- [ ] **Phase 8: Test and Constant Cleanup** - 1 plan — Remove stale constants and update test bounds that would block all subsequent v1.2 work
- [ ] **Phase 9: Frequency Design and Group Architecture** - Design complete Hz allocation for all ~35 classes in family bands and add Group field to FreqConfig
- [ ] **Phase 10: Classification Layer** - Add ~21 new TrafficClass constants and port rules covering all new protocol families
- [ ] **Phase 11: Synthesis and Config Layer** - Add ClassFreqConfigs entries for all new classes, update auto-assign range, and add group-header output to --print-config
@@ -50,7 +50,9 @@ Full details: `.planning/milestones/v1.1-ROADMAP.md`
1. `go test ./...` passes on main with no modifications to the new v1.2 code path
2. The stale `NumLayers`/`GainPerLayer` exported constant no longer exists in the synth package — callers cannot accidentally use it
3. `TestFrequenciesInRange` accepts the new extended Hz range without manual test surgery when new classes are added in Phase 10
-**Plans**: TBD
+**Plans:** 1 plan
+Plans:
+- [ ] 08-01-PLAN.md — Delete stale constants, future-proof test bounds, rename/consolidate tests
### Phase 9: Frequency Design and Group Architecture
**Goal**: A complete, documented frequency allocation table for all ~35 traffic classes exists and the FreqConfig struct carries a Group field — design decisions are locked in before any protocol code is written
@@ -97,7 +99,7 @@ Full details: `.planning/milestones/v1.1-ROADMAP.md`
| 5. Waveform Types and Bank Decoupling | v1.1 | 2/2 | Complete | 2026-03-26 |
| 6. Config Package and Sound Overrides | v1.1 | 2/2 | Complete | 2026-03-26 |
| 7. Custom Rules and Print-Config | v1.1 | 2/2 | Complete | 2026-03-26 |
-| 8. Test and Constant Cleanup | v1.2 | 0/? | Not started | - |
+| 8. Test and Constant Cleanup | v1.2 | 0/1 | Planning | - |
| 9. Frequency Design and Group Architecture | v1.2 | 0/? | Not started | - |
| 10. Classification Layer | v1.2 | 0/? | Not started | - |
| 11. Synthesis and Config Layer | v1.2 | 0/? | Not started | - |
diff --git a/.planning/phases/08-test-and-constant-cleanup/08-01-PLAN.md b/.planning/phases/08-test-and-constant-cleanup/08-01-PLAN.md
new file mode 100644
index 0000000..6659f7f
--- /dev/null
+++ b/.planning/phases/08-test-and-constant-cleanup/08-01-PLAN.md
@@ -0,0 +1,214 @@
+---
+phase: 08-test-and-constant-cleanup
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - synth/config.go
+ - synth/config_test.go
+autonomous: true
+requirements:
+ - CLEAN-01
+must_haves:
+ truths:
+ - "NumLayers and GainPerLayer constants do not exist in the synth package"
+ - "go test ./... passes with zero failures after all edits"
+ - "TestFrequenciesInRange accepts any BaseHz in (0, Nyquist) without manual test surgery"
+ - "TestNumLayersMatchesAllClasses is renamed to TestClassFreqConfigsMatchAllClasses"
+ artifacts:
+ - path: "synth/config.go"
+ provides: "Cleaned constant block without NumLayers or GainPerLayer"
+ contains: "WhisperFloor"
+ - path: "synth/config_test.go"
+ provides: "Future-proof test assertions"
+ contains: "TestClassFreqConfigsMatchAllClasses"
+ key_links:
+ - from: "synth/bank.go"
+ to: "synth/config.go"
+ via: "gainPerLayer computed dynamically in NewBank — no longer any static constant to conflict with"
+ pattern: "1\\.0 / float64\\(len\\(cfgs\\)\\)"
+ - from: "synth/config_test.go"
+ to: "synth/config.go"
+ via: "TestFrequenciesInRange validates BaseHz against SampleRate-derived Nyquist"
+ pattern: "float64\\(synth\\.SampleRate\\)"
+---
+
+
+Remove stale exported constants and hardcoded test bounds from the synth package so that subsequent v1.2 phases can add new traffic classes and frequencies without triggering false CI failures.
+
+Purpose: Phase 8 is the gatekeeper for all v1.2 work. NumLayers=14 is dead code (NewBank computes gain dynamically), and TestFrequenciesInRange's [60, 1100] bound will reject any new class above 1100 Hz. Both must be cleaned up before Phase 9-11 proceed.
+
+Output: Two edited files (synth/config.go, synth/config_test.go) with all tests green.
+
+
+
+@$HOME/.claude/get-shit-done/workflows/execute-plan.md
+@$HOME/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/08-test-and-constant-cleanup/08-CONTEXT.md
+@.planning/phases/08-test-and-constant-cleanup/08-RESEARCH.md
+
+
+
+
+From synth/config.go (current constant block, lines 5-12):
+```go
+const (
+ SampleRate = 44100 // D-13: CD quality
+ WindowMs = 500 // matches aggregate.DefaultWindowMs
+ SamplesPerWindow = SampleRate * WindowMs / 1000 // 22050
+ NumLayers = 14 // <-- DELETE per D-01
+ GainPerLayer = 1.0 / float64(NumLayers) // D-10: ~0.0714 <-- DELETE per D-01
+ WhisperFloor = 0.03 // D-08/D-09: 3% of max amplitude
+)
+```
+
+From synth/bank.go (line 21 — confirms constants are dead code):
+```go
+gainPerLayer: 1.0 / float64(len(cfgs)), // dynamic computation, no constant needed
+```
+
+From synth/config_test.go (current test functions to modify):
+```go
+// Lines 18-25: TestFrequenciesInRange — hardcoded [60, 1100] to replace
+// Lines 53-59: TestClassFreqConfigsComplete — duplicate of TestAllClassesHaveConfig
+// Lines 61-66: TestNumLayersMatchesAllClasses — rename to TestClassFreqConfigsMatchAllClasses
+```
+
+
+
+
+
+
+ Task 1: Delete stale NumLayers and GainPerLayer constants from synth/config.go
+ synth/config.go
+
+ - synth/config.go (see current constant block at lines 5-12)
+ - synth/bank.go (line 21 confirms gainPerLayer is computed dynamically)
+
+
+Per D-01: Delete lines 9-10 from synth/config.go — the `NumLayers = 14` and `GainPerLayer = 1.0 / float64(NumLayers)` constants.
+
+The resulting constant block must be:
+
+```go
+const (
+ SampleRate = 44100 // D-13: CD quality
+ WindowMs = 500 // matches aggregate.DefaultWindowMs
+ SamplesPerWindow = SampleRate * WindowMs / 1000 // 22050
+ WhisperFloor = 0.03 // D-08/D-09: 3% of max amplitude
+)
+```
+
+Do NOT modify any other lines in config.go. Do NOT touch bank.go — NewBank's dynamic `1.0 / float64(len(cfgs))` is already correct.
+
+Optionally, add a traceability comment to synth/bank.go line 21:
+`gainPerLayer: 1.0 / float64(len(cfgs)), // D-10: 1/N so all layers at full amplitude sum to 1.0`
+This is low-priority — skip if it feels like noise.
+
+
+ cd /home/dev/workspace/yoloyolo && grep -n "NumLayers\|GainPerLayer" synth/config.go; echo "EXIT:$?"
+
+
+ - `grep -c "NumLayers" synth/config.go` returns 0
+ - `grep -c "GainPerLayer" synth/config.go` returns 0
+ - `synth/config.go` still contains `SampleRate = 44100`
+ - `synth/config.go` still contains `WhisperFloor = 0.03`
+ - `go build ./synth/...` exits 0 (no compilation errors from removing the constants)
+
+ NumLayers and GainPerLayer constants no longer exist in synth/config.go. The constant block contains only SampleRate, WindowMs, SamplesPerWindow, and WhisperFloor. Code compiles cleanly.
+
+
+
+ Task 2: Update synth/config_test.go — future-proof frequency bounds, rename test, remove duplicate
+ synth/config_test.go
+
+ - synth/config_test.go (full file — see all 6 test functions)
+ - synth/config.go (after Task 1 edits — confirm SampleRate is exported)
+
+
+Three changes to synth/config_test.go:
+
+**Change 1 (per D-02):** Replace the body of `TestFrequenciesInRange` (lines 18-25) with Nyquist-based validation. The new function body:
+
+```go
+func TestFrequenciesInRange(t *testing.T) {
+ const nyquist = float64(synth.SampleRate) / 2.0 // 22050 Hz
+ for class, cfg := range synth.ClassFreqConfigs {
+ if cfg.BaseHz <= 0 {
+ t.Errorf("class %q BaseHz=%.1f must be positive", class, cfg.BaseHz)
+ }
+ if cfg.BaseHz >= nyquist {
+ t.Errorf("class %q BaseHz=%.1f exceeds Nyquist (%.1f Hz)", class, cfg.BaseHz, nyquist)
+ }
+ }
+}
+```
+
+This accepts any BaseHz in (0, 22050) — no manual edit needed when Phase 10 adds classes above 1100 Hz.
+
+**Change 2 (per D-03):** Rename `TestNumLayersMatchesAllClasses` (line 61) to `TestClassFreqConfigsMatchAllClasses`. Keep the function body identical:
+
+```go
+func TestClassFreqConfigsMatchAllClasses(t *testing.T) {
+ if len(synth.ClassFreqConfigs) != len(classify.AllClasses()) {
+ t.Errorf("ClassFreqConfigs has %d entries but AllClasses() has %d entries",
+ len(synth.ClassFreqConfigs), len(classify.AllClasses()))
+ }
+}
+```
+
+**Change 3 (Claude's discretion — consolidation):** Delete `TestClassFreqConfigsComplete` (lines 53-59) entirely. It is a semantic duplicate of `TestAllClassesHaveConfig` (lines 10-16) — both iterate `AllClasses()` and check for a map entry. Removing it leaves three non-overlapping tests:
+- `TestAllClassesHaveConfig` — every AllClasses() member has a map entry
+- `TestClassFreqConfigsMatchAllClasses` — count parity (catches extra entries)
+- `TestFrequenciesInRange` — all BaseHz positive and below Nyquist
+
+The remaining tests (`TestFrequenciesUnique`, `TestHarmonicsNonEmpty`, `TestPanPositionsInRange`) are untouched.
+
+
+ cd /home/dev/workspace/yoloyolo && go test ./synth/... -v -count=1 2>&1 | head -40
+
+
+ - `grep -c "TestNumLayersMatchesAllClasses" synth/config_test.go` returns 0
+ - `grep -c "TestClassFreqConfigsMatchAllClasses" synth/config_test.go` returns 1
+ - `grep -c "TestClassFreqConfigsComplete" synth/config_test.go` returns 0
+ - `grep "nyquist" synth/config_test.go` returns at least one match
+ - `grep "1100" synth/config_test.go` returns 0 matches (hardcoded bound removed)
+ - `go test ./synth/... -run TestFrequenciesInRange` exits 0
+ - `go test ./synth/... -run TestClassFreqConfigsMatchAllClasses` exits 0
+ - `go test ./...` exits 0 (full suite green)
+
+ TestFrequenciesInRange uses Nyquist-based validation (no hardcoded upper bound). TestNumLayersMatchesAllClasses renamed to TestClassFreqConfigsMatchAllClasses. Duplicate TestClassFreqConfigsComplete removed. Full test suite passes.
+
+
+
+
+
+After both tasks complete:
+
+1. `grep -rn "NumLayers\|GainPerLayer" synth/*.go` — zero matches in production and test code
+2. `grep -n "1100" synth/config_test.go` — zero matches (hardcoded bound gone)
+3. `go test ./...` — all packages pass with zero failures
+4. `go test ./synth/... -run TestFrequenciesInRange` — passes
+5. `go test ./synth/... -run TestClassFreqConfigsMatchAllClasses` — passes
+6. `go test ./synth/... -run TestClassFreqConfigsComplete` — no such test (removed)
+
+
+
+- NumLayers and GainPerLayer constants deleted from synth/config.go
+- TestFrequenciesInRange validates against Nyquist (22050 Hz), not hardcoded 1100
+- TestNumLayersMatchesAllClasses renamed to TestClassFreqConfigsMatchAllClasses
+- Duplicate TestClassFreqConfigsComplete removed
+- `go test ./...` passes with zero failures
+- Adding a new class with BaseHz=2000 in a future phase would NOT require editing any test
+
+
+