Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
13 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 07-custom-rules-and-print-config | 01 | tdd | 1 |
|
true |
|
|
Purpose: This is the data layer for user-defined classification rules (RULE-01, RULE-02, RULE-03). The LoadResult struct becomes the contract consumed by Plan 02 for CLI wiring and print-config.
Output: Updated config/config.go with RawRule, LoadResult, validation, auto-freq; comprehensive tests in config/config_test.go.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/07-custom-rules-and-print-config/07-CONTEXT.md @.planning/phases/07-custom-rules-and-print-config/07-RESEARCH.md@config/config.go @config/config_test.go @classify/rules.go @classify/types.go @synth/config.go
From classify/rules.go:
type Rule struct {
Protocol string
DstPort uint16
Class TrafficClass
}
var DefaultRules = []Rule{ ... } // 12 rules, first-match-wins
From classify/types.go:
type TrafficClass string
func AllClasses() []TrafficClass // returns 14 built-in classes
From synth/config.go:
type WaveformType int
const WaveformSine WaveformType = 1
type FreqConfig struct {
BaseHz float64
Harmonics []HarmonicDef
Pan float64
WaveformType WaveformType
}
var ClassFreqConfigs = map[classify.TrafficClass]FreqConfig{ ... } // 14 entries, max 1047 Hz
func WaveformPresetHarmonics(wt WaveformType, baseHz float64, sampleRate int) []HarmonicDef
const SampleRate = 44100
From config/config.go (current):
type SoundOverride struct {
Frequency *float64 `toml:"frequency"`
Waveform *string `toml:"waveform"`
}
type rawConfig struct {
Sounds map[string]SoundOverride `toml:"sounds"`
}
func Load(configPath string) (map[classify.TrafficClass]synth.FreqConfig, error)
From config/config_test.go (patterns):
func writeTOML(t *testing.T, content string) string // creates temp TOML file
// Tests call config.Load(path) and check returned map
GREEN phase -- Modify config/config.go:
- Add RawRule struct (per D-01):
type RawRule struct {
Port *uint16 `toml:"port"`
Protocol string `toml:"protocol"`
Class string `toml:"class"`
}
- Add Rules field to rawConfig:
type rawConfig struct {
Sounds map[string]SoundOverride `toml:"sounds"`
Rules []RawRule `toml:"rules"`
}
- Add LoadResult struct (per D-13, Claude's Discretion: struct over tuple):
type LoadResult struct {
FreqCfgs map[classify.TrafficClass]synth.FreqConfig
UserRules []classify.Rule
ConfigPath string
}
- Add validateRules function called from validate():
func validateRules(rules []RawRule) error {
validProtocols := map[string]bool{"tcp": true, "udp": true, "icmp": true}
for i, r := range rules {
if r.Protocol == "" {
return fmt.Errorf("config: rules[%d]: protocol is required", i)
}
if !validProtocols[r.Protocol] {
return fmt.Errorf("config: rules[%d]: invalid protocol %q -- valid: tcp, udp, icmp", i, r.Protocol)
}
if r.Class == "" {
return fmt.Errorf("config: rules[%d]: class is required", i)
}
}
return nil
}
- Add convertRules function:
func convertRules(raw []RawRule) []classify.Rule {
result := make([]classify.Rule, len(raw))
for i, r := range raw {
var port uint16
if r.Port != nil {
port = *r.Port
}
result[i] = classify.Rule{
Protocol: r.Protocol,
DstPort: port,
Class: classify.TrafficClass(r.Class),
}
}
return result
}
- Add autoAssignFreq function (per D-07/D-08, using FNV-32a):
import "hash/fnv"
func autoAssignFreq(className string) float64 {
h := fnv.New32a()
h.Write([]byte(className))
const (
baseHz = 1200.0
stepHz = 50.0
numSteps = uint32(24)
)
return baseHz + float64(h.Sum32()%numSteps)*stepHz
}
- Add addAutoFreqEntries function (called AFTER merge, per Pitfall 4):
func addAutoFreqEntries(cfgs map[classify.TrafficClass]synth.FreqConfig, userRules []classify.Rule) {
for _, rule := range userRules {
if _, exists := cfgs[rule.Class]; !exists {
baseHz := autoAssignFreq(string(rule.Class))
cfgs[rule.Class] = synth.FreqConfig{
BaseHz: baseHz,
WaveformType: synth.WaveformSine,
Harmonics: synth.WaveformPresetHarmonics(synth.WaveformSine, baseHz, synth.SampleRate),
Pan: 0.0,
}
}
}
}
- Change Load() signature to return LoadResult:
func Load(configPath string) (LoadResult, error) {
- When no config found: return
LoadResult{FreqCfgs: copyDefaults(), ConfigPath: ""}, nil - After parseFile + validate + merge: call convertRules, call addAutoFreqEntries, return LoadResult with FreqCfgs, UserRules, and the resolved config path
- Update validate() to also call validateRules(raw.Rules)
- The
merge()function for unknown class names should NO LONGER print a warning for classes that appear in raw.Rules -- those are legitimate custom classes. Keep the warning only for [sounds.X] where X is neither a built-in class nor a class defined in rules.
- Update the merge() function: Change the unknown-class warning logic. Instead of always warning on unknown class names in sounds, accept the
userRules []RawRuleas a parameter (or check after conversion). Simplest: after converting rules, pass the set of user-defined class names to merge so it can skip the warning for those. Alternatively, run merge first (with warnings), then let addAutoFreqEntries handle user-defined classes. The warning is acceptable for now -- it only fires for [sounds.X] where X has no matching rules entry AND is not a built-in class. Keep existing warning behavior, it is harmless.
REFACTOR phase -- Clean up if needed. Ensure all tests pass.
Run go test ./config/... -- all tests must pass.
Run go test ./... -- full suite must pass (the Load() call site in main.go will break; that is expected and fixed in Plan 02).
go test ./config/... -v -count=1
<acceptance_criteria>
- config/config.go contains type RawRule struct with Port *uint16, Protocol string, Class string fields
- config/config.go contains type LoadResult struct with FreqCfgs, UserRules, ConfigPath fields
- config/config.go contains func Load(configPath string) (LoadResult, error)
- config/config.go contains func validateRules(rules []RawRule) error
- config/config.go contains func convertRules(raw []RawRule) []classify.Rule
- config/config.go contains func autoAssignFreq(className string) float64 with fnv.New32a()
- config/config.go contains func addAutoFreqEntries(
- config/config.go imports "hash/fnv"
- config/config_test.go contains TestLoadCustomRules
- config/config_test.go contains TestAutoFreqAssignment
- config/config_test.go contains TestAutoFreqSkipsBuiltins
- config/config_test.go contains TestLoadCustomRuleMissingProtocol
- go test ./config/... -count=1 exits 0
</acceptance_criteria>
Load() returns LoadResult with FreqCfgs + UserRules + ConfigPath. TOML rules blocks parse, validate (protocol required, class required, valid protocols only), and convert to classify.Rule slices. Auto-frequency assignment creates FreqConfig entries for new class names in 1200-2350 Hz range using FNV-32a. Built-in class names from user rules are NOT overwritten. All existing config tests updated and passing. Full config test suite green.
<success_criteria>
- config.Load() returns LoadResult struct (not bare map)
- rules TOML blocks parse into UserRules field
- Validation catches missing protocol, missing class, invalid protocol
- Auto-freq assigns 1200-2350 Hz for new class names, skips built-ins
- All 8 existing config tests updated and passing
- All new tests passing </success_criteria>