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>
17 KiB
Phase 8: Test and Constant Cleanup - Research
Researched: 2026-03-27 Domain: Go test cleanup, dead code removal, test assertion generalization Confidence: HIGH
<user_constraints>
User Constraints (from CONTEXT.md)
Locked Decisions
- D-01: Delete
NumLayersandGainPerLayerconstants entirely fromsynth/config.go. They are dead code —NewBankalready computesgainPerLayerdynamically as1.0 / float64(len(cfgs))(bank.go:21). No external callers reference either constant outside the test file. - D-02: Replace the hardcoded
[60, 1100]bounds inTestFrequenciesInRangewith dynamic validation — derive the valid range from theClassFreqConfigsdata itself (e.g., check that all frequencies are positive and below Nyquist) rather than hardcoding a new magic number that would need manual updating when Phase 9/10 add classes above 1100 Hz. - D-03: Rename
TestNumLayersMatchesAllClassestoTestClassFreqConfigsMatchAllClasses(or similar) to reflect the actual invariant being tested afterNumLayersremoval.
Claude's Discretion
- Whether to use a generous static upper bound vs a computed Nyquist-based bound for D-02 — either approach satisfies the constraint
- Whether
WhisperFlooror other constants in config.go need any adjustment (they don't reference NumLayers, so likely no) - Whether
TestClassFreqConfigsComplete(line 53) should be consolidated with the renamed test since both verify the same invariant
Deferred Ideas (OUT OF SCOPE)
- Adding protocols (IMAP, POP3, SNMP, FTP, etc.) — this is Phase 10 scope; Phase 8 only removes blockers </user_constraints>
<phase_requirements>
Phase Requirements
| ID | Description | Research Support |
|---|---|---|
| CLEAN-01 | Remove stale NumLayers constant and hardcoded frequency range test assertions that would block new class additions |
D-01 removes the constants; D-02 replaces hardcoded bounds with future-proof validation; D-03 renames stale test function |
| </phase_requirements> |
Summary
Phase 8 is a pure cleanup phase with two narrowly scoped targets: (1) two dead exported constants in synth/config.go and (2) three test functions in synth/config_test.go that need renaming or rewiring. No new features, no new packages, no new dependencies.
NumLayers = 14 and GainPerLayer = 1.0 / float64(NumLayers) in synth/config.go are provably dead code. synth/bank.go:NewBank computes gainPerLayer dynamically at line 21 as 1.0 / float64(len(cfgs)). Neither constant is referenced anywhere in the production code path — only in the test file's function name TestNumLayersMatchesAllClasses (which itself does not use either constant in its body). Deleting both constants removes a misleading signal and eliminates the risk of future callers accidentally hardcoding the stale count 14.
TestFrequenciesInRange asserts cfg.BaseHz < 60 || cfg.BaseHz > 1100. Phase 9 will redistribute frequencies and Phase 10 will add classes whose frequencies will exceed 1100 Hz. The test will produce false failures the moment any ClassFreqConfigs entry above 1100 Hz is added. Replacing the hardcoded upper bound with a Nyquist-based check (or a generous static bound like 8000 Hz) makes the test structurally future-proof without encoding new domain knowledge in this phase.
Primary recommendation: Three surgical edits to two files — delete 2 lines in config.go, update 1 test function body + rename 2 test functions in config_test.go. Total change surface is under 15 lines.
Standard Stack
No new dependencies. This phase touches only existing Go source files.
Core
| Library | Version | Purpose | Why Standard |
|---|---|---|---|
testing |
stdlib | Test assertions | Already used throughout the codebase |
Installation: None required — no new packages.
Architecture Patterns
Files Modified (exhaustive list)
synth/
├── config.go # Delete NumLayers and GainPerLayer constants (lines 9-10)
└── config_test.go # Update TestFrequenciesInRange body; rename two test functions
No other files are modified. The CONTEXT.md explicitly states: "Only synth/config.go and synth/config_test.go are modified — no downstream package changes expected."
Pattern 1: Dead Constant Removal
What: Delete lines 9-10 from synth/config.go.
Current state:
// synth/config.go lines 5-12
const (
SampleRate = 44100
WindowMs = 500
SamplesPerWindow = SampleRate * WindowMs / 1000 // 22050
NumLayers = 14
GainPerLayer = 1.0 / float64(NumLayers) // D-10: ~0.0714
WhisperFloor = 0.03
)
After deletion:
const (
SampleRate = 44100
WindowMs = 500
SamplesPerWindow = SampleRate * WindowMs / 1000 // 22050
WhisperFloor = 0.03
)
Verification: grep -r "NumLayers\|GainPerLayer" . must return zero hits in *.go files after deletion. The only non-test reference is in .planning/research/ARCHITECTURE.md (planning docs — not compiled).
Pattern 2: Nyquist-Based Frequency Range Validation
What: Replace the hardcoded [60, 1100] upper bound in TestFrequenciesInRange.
Recommended approach (Nyquist-based): Phase 9 will add classes up to ~4000 Hz. Nyquist at 44100 Hz sample rate is 22050 Hz. A Nyquist check is mathematically correct and never needs updating regardless of how many new classes are added:
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)
}
}
}
Alternative approach (generous static bound): A bound of [20, 8000] also satisfies D-02's constraint since all planned Phase 9/10 frequencies are under 4000 Hz. However the Nyquist approach is self-documenting — it explains why there's an upper bound rather than encoding an arbitrary number. Either is acceptable per Claude's Discretion.
Key invariant preserved: Both approaches ensure adding a new class at any humanly-audible frequency (20 Hz – 20 kHz, well within Nyquist) will NOT require editing this test.
Pattern 3: Test Function Rename
What: Rename TestNumLayersMatchesAllClasses at line 61. The test body already tests the correct invariant (len(synth.ClassFreqConfigs) == len(classify.AllClasses())); only the name is stale.
Current:
func TestNumLayersMatchesAllClasses(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()))
}
}
After rename:
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()))
}
}
Pattern 4: Consolidation Decision (Claude's Discretion)
TestAllClassesHaveConfig (lines 10-16) and TestClassFreqConfigsComplete (lines 53-59) test the same invariant: every class in AllClasses() has an entry in ClassFreqConfigs. They are exact duplicates in semantics (different error messages but identical logic). The renamed TestClassFreqConfigsMatchAllClasses (formerly TestNumLayersMatchesAllClasses) tests the converse: lengths match.
Recommendation: Remove TestClassFreqConfigsComplete (lines 53-59) as a duplicate of TestAllClassesHaveConfig. This leaves three non-overlapping coverage tests:
TestAllClassesHaveConfig— every AllClasses() member has a map entryTestClassFreqConfigsMatchAllClasses— count parity (catches extra entries not in AllClasses)TestFrequenciesInRange— all BaseHz values are positive and below Nyquist
Alternatively, leave both functions if deduplication is not worth the discussion. Both pass and both protect the invariant. This is truly Claude's discretion.
Anti-Patterns to Avoid
- Updating
NumLayersinstead of deleting it: The decision (D-01) is deletion, not update. An updated constant would still be a maintenance burden. - Replacing [60, 1100] with [60, 4000]: A new hardcoded number has the same fragility as the old one — it becomes stale when the frequency spectrum changes again in a future milestone.
- Touching
bank.go: The dynamicgainPerLayercomputation inbank.gois already correct. No changes needed. - Touching
classify/types.go:AllClasses()is not modified in this phase. - Touching
WhisperFloor: It does not referenceNumLayersorGainPerLayer; leave it unchanged.
Don't Hand-Roll
Not applicable. This phase contains no algorithmic code — it is deletion and test rewriting.
Common Pitfalls
Pitfall 1: Leaving the GainPerLayer comment reference orphaned
What goes wrong: GainPerLayer at config.go line 10 has a comment // D-10: ~0.0714. After deletion, the decision reference D-10 disappears from the source. This is fine — D-10 is still documented in the planning research files. But if the comment is moved to bank.go line 21 (where the dynamic computation lives), it improves traceability without leaving an orphan.
How to avoid: Either delete both lines cleanly with no compensation, or add // D-10: gain is 1/N computed dynamically to bank.go:21. Both are acceptable.
Warning signs: Go compiler catches unused constants — if NumLayers or GainPerLayer are deleted and the code still compiles, they were indeed dead.
Pitfall 2: Using synth.SampleRate in the test without verifying the export
What goes wrong: SampleRate is an exported constant in synth/config.go. The test file is in package synth_test (external test package), so it accesses synth.SampleRate. Verify SampleRate is exported (capital S) before referencing it from the test.
How to avoid: Already confirmed — SampleRate = 44100 is exported at config.go line 6. No issue.
Warning signs: Compiler error synth.sampleRate undefined would indicate a lowercase constant.
Pitfall 3: Test duplication confusion
What goes wrong: TestAllClassesHaveConfig and TestClassFreqConfigsComplete look different but test the same invariant. During code review or future debugging, someone might wonder why there are two tests for the same thing.
How to avoid: If consolidating (removing TestClassFreqConfigsComplete), add a comment to TestAllClassesHaveConfig noting it replaced the duplicate. If not consolidating, no action needed.
Code Examples
Resulting synth/config.go constant block
// Source: synth/config.go — after Phase 8 cleanup
const (
SampleRate = 44100
WindowMs = 500
SamplesPerWindow = SampleRate * WindowMs / 1000 // 22050
WhisperFloor = 0.03 // D-08/D-09: 3% of max amplitude
)
Resulting TestFrequenciesInRange (Nyquist approach)
// Source: synth/config_test.go — after Phase 8 cleanup
func TestFrequenciesInRange(t *testing.T) {
const nyquist = float64(synth.SampleRate) / 2.0
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)
}
}
}
Resulting TestClassFreqConfigsMatchAllClasses
// Source: synth/config_test.go — after rename from TestNumLayersMatchesAllClasses
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()))
}
}
State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|---|---|---|---|
NumLayers = 14 static constant |
Dynamic 1.0 / float64(len(cfgs)) in NewBank |
Phase 6/7 (v1.1) | Static constant is now dead code; remove it |
TestFrequenciesInRange checks [60, 1100] |
Nyquist-based check (this phase) | Phase 8 (v1.2) | Test survives any future frequency allocation |
Validation Architecture
Test Framework
| Property | Value |
|---|---|
| Framework | testing stdlib, Go 1.24 |
| Config file | none (standard go test) |
| Quick run command | go test ./synth/... |
| Full suite command | go test ./... |
Phase Requirements → Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|---|---|---|---|---|
| CLEAN-01 (constant removal) | NumLayers and GainPerLayer are not exported from synth package |
unit — compile check | go build ./synth/... |
✅ (config.go exists; delete lines) |
| CLEAN-01 (no broken references) | Full test suite passes after deletion | integration | go test ./... |
✅ |
| CLEAN-01 (range test future-proof) | TestFrequenciesInRange passes with any BaseHz in (0, Nyquist) range |
unit | go test ./synth/... -run TestFrequenciesInRange |
✅ (config_test.go exists; update body) |
| CLEAN-01 (test rename) | TestClassFreqConfigsMatchAllClasses exists and passes |
unit | go test ./synth/... -run TestClassFreqConfigsMatchAllClasses |
✅ (rename existing function) |
Sampling Rate
- Per task commit:
go test ./synth/... - Per wave merge:
go test ./... - Phase gate:
go test ./...green before/gsd:verify-work
Wave 0 Gaps
None — existing test infrastructure covers all phase requirements. No new test files, fixtures, or framework setup needed.
Environment Availability
Step 2.6: SKIPPED (no external dependencies — pure Go source edits, no new tools or services required).
Current test suite state confirmed: go test ./... passes on all 7 packages.
Open Questions
-
Consolidate
TestAllClassesHaveConfigandTestClassFreqConfigsComplete?- What we know: They test the same invariant; both currently pass; no correctness issue either way
- What's unclear: Whether the planner wants one clean authoritative test or is fine leaving both
- Recommendation: Remove
TestClassFreqConfigsComplete(lines 53-59) as it duplicatesTestAllClassesHaveConfig. The namedTestAllClassesHaveConfigis more expressive. If this causes any concern, leave both — both are correct.
-
Add D-10 comment to
bank.goafter deletingGainPerLayer?- What we know:
GainPerLayercarries// D-10: ~0.0714; bank.go line 21 is where the actual computation lives - What's unclear: Whether the project wants decision-reference comments preserved at the implementation site
- Recommendation: Add
// D-10: gainPerLayer = 1/N so all N layers at full amplitude sum to 1.0to bank.go line 21. Low-cost, improves traceability.
- What we know:
Sources
Primary (HIGH confidence)
synth/config.go— Direct inspection:NumLayers = 14,GainPerLayer = 1.0 / float64(NumLayers)at lines 9-10;SampleRate = 44100at line 6synth/bank.go— Direct inspection:gainPerLayer: 1.0 / float64(len(cfgs))at line 21 — confirms constants are dead codesynth/config_test.go— Direct inspection:TestFrequenciesInRangebody at lines 18-25;TestNumLayersMatchesAllClassesat lines 61-66;TestClassFreqConfigsCompleteat lines 53-59classify/types.go— Direct inspection:AllClasses()returns 14 entries;SampleRate = 44100used for Nyquist calculation.planning/phases/08-test-and-constant-cleanup/08-CONTEXT.md— Locked decisions D-01, D-02, D-03.planning/research/PITFALLS.md— Pitfall C3 (NumLayers stale constant) and C4 (TestFrequenciesInRange hardcoding).planning/research/ARCHITECTURE.mdlines 414-422 — NumLayers/GainPerLayer dead code analysis
Secondary (MEDIUM confidence)
go test ./...output — All 7 packages pass; current baseline confirmed
Metadata
Confidence breakdown:
- Standard stack: HIGH — no new dependencies; pure stdlib
- Architecture: HIGH — all target lines verified by direct file inspection
- Pitfalls: HIGH — sourced from project research files and direct code inspection
Research date: 2026-03-27
Valid until: Until Phase 9 begins (frequency redistribution) — this research is tied to current synth/config.go line numbers which Phase 9 will change