Files
yoloyolo/.planning/phases/07-custom-rules-and-print-config/07-01-PLAN.md
T
2026-03-26 21:38:56 +01:00

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
config/config.go
config/config_test.go
true
RULE-01
RULE-02
RULE-03
truths artifacts key_links
TOML rules blocks parse into classify.Rule slices
Missing protocol or class in a rule produces a clear error at startup
User rules are returned separately from FreqCfgs for caller to prepend
New class names without explicit sound config get auto-assigned frequencies in 1200-2400 Hz range
Built-in class names in user rules do not get overwritten by auto-freq
path provides contains
config/config.go RawRule, LoadResult, validateRules, convertRules, autoAssignFreq, addAutoFreqEntries type LoadResult struct
path provides contains
config/config_test.go Tests for rule parsing, validation, auto-freq, LoadResult TestLoadCustomRules
from to via pattern
config/config.go classify/rules.go convertRules produces []classify.Rule classify.Rule
from to via pattern
config/config.go synth/config.go autoAssignFreq creates FreqConfig entries with WaveformPresetHarmonics synth.WaveformPresetHarmonics
Extend the config package to parse `rules` TOML blocks into classification rules, validate them, auto-assign frequencies for new class names, and return a `LoadResult` struct from `Load()`.

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
Task 1: RawRule, LoadResult, validation, conversion, and auto-freq with TDD config/config.go, config/config_test.go config/config.go, config/config_test.go, classify/rules.go, classify/types.go, synth/config.go - TestLoadCustomRules: TOML with `rules` block (port=8080, protocol="tcp", class="MyApp") + `[sounds.MyApp]` (frequency=300.0) parses successfully; LoadResult.UserRules has len 1 with Protocol="tcp", DstPort=8080, Class="MyApp"; LoadResult.FreqCfgs["MyApp"].BaseHz == 300.0 - TestLoadCustomRuleNoPort: TOML with `rules` (protocol="udp", class="AllUDP", no port field) parses; UserRules[0].DstPort == 0 - TestLoadCustomRuleMissingProtocol: TOML `rules` with class="X" but no protocol field -> error containing "protocol is required" - TestLoadCustomRuleMissingClass: TOML `rules` with protocol="tcp" but no class field -> error containing "class is required" - TestLoadCustomRuleInvalidProtocol: TOML `rules` with protocol="ftp" -> error containing "invalid protocol" - TestLoadCustomRuleUnknownField: TOML `rules` with typo_field="bad" -> error containing "typo_field" (from Undecoded()) - TestUserRulesPrepend: Load returns UserRules separately from FreqCfgs; caller can do `append(result.UserRules, classify.DefaultRules...)` to get user rules first - TestAutoFreqAssignment: TOML with `rules` (class="GameServer", protocol="tcp") and NO `[sounds.GameServer]` -> FreqCfgs contains "GameServer" entry with BaseHz in range [1200, 2350] and WaveformType == WaveformSine - TestAutoFreqDeterministic: Two Load() calls with same class name produce same BaseHz - TestAutoFreqSkipsBuiltins: TOML with `rules` (class="HTTPS", protocol="tcp", port=443) -> FreqCfgs["HTTPS"].BaseHz == 175.0 (the default), NOT an auto-assigned value - TestLoadResultConfigPath: Load(explicit_path) -> LoadResult.ConfigPath == explicit_path; Load("") with no file -> LoadResult.ConfigPath == "" - TestLoadNoConfigReturnsLoadResult: Load("") in empty dir returns LoadResult with len(FreqCfgs)==14, len(UserRules)==0, ConfigPath=="" - TestExistingTestsStillPass: All 8 existing tests in config_test.go continue to pass after Load() signature change (they need updating to use LoadResult) RED phase -- Write all test functions listed in behavior above in config/config_test.go. Tests call config.Load() and assert on LoadResult fields. Update the 8 existing tests to use the new LoadResult return type (e.g., `result, err := config.Load(path); cfgs := result.FreqCfgs`). Run tests -- they must all fail (Load still returns bare map).

GREEN phase -- Modify config/config.go:

  1. Add RawRule struct (per D-01):
type RawRule struct {
    Port     *uint16 `toml:"port"`
    Protocol string  `toml:"protocol"`
    Class    string  `toml:"class"`
}
  1. Add Rules field to rawConfig:
type rawConfig struct {
    Sounds map[string]SoundOverride `toml:"sounds"`
    Rules  []RawRule                `toml:"rules"`
}
  1. 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
}
  1. 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
}
  1. 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
}
  1. 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
}
  1. 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,
            }
        }
    }
}
  1. 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.
  1. Update the merge() function: Change the unknown-class warning logic. Instead of always warning on unknown class names in sounds, accept the userRules []RawRule as 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.

- `go test ./config/... -v -count=1` passes all tests including new rule-related tests - `go vet ./config/...` reports no issues - LoadResult struct is exported and usable from cmd/netsynth package

<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>
After completion, create `.planning/phases/07-custom-rules-and-print-config/07-01-SUMMARY.md`