Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
303 lines
13 KiB
Markdown
303 lines
13 KiB
Markdown
---
|
|
phase: 07-custom-rules-and-print-config
|
|
plan: 01
|
|
type: tdd
|
|
wave: 1
|
|
depends_on: []
|
|
files_modified:
|
|
- config/config.go
|
|
- config/config_test.go
|
|
autonomous: true
|
|
requirements:
|
|
- RULE-01
|
|
- RULE-02
|
|
- RULE-03
|
|
|
|
must_haves:
|
|
truths:
|
|
- "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"
|
|
artifacts:
|
|
- path: "config/config.go"
|
|
provides: "RawRule, LoadResult, validateRules, convertRules, autoAssignFreq, addAutoFreqEntries"
|
|
contains: "type LoadResult struct"
|
|
- path: "config/config_test.go"
|
|
provides: "Tests for rule parsing, validation, auto-freq, LoadResult"
|
|
contains: "TestLoadCustomRules"
|
|
key_links:
|
|
- from: "config/config.go"
|
|
to: "classify/rules.go"
|
|
via: "convertRules produces []classify.Rule"
|
|
pattern: "classify\\.Rule"
|
|
- from: "config/config.go"
|
|
to: "synth/config.go"
|
|
via: "autoAssignFreq creates FreqConfig entries with WaveformPresetHarmonics"
|
|
pattern: "synth\\.WaveformPresetHarmonics"
|
|
---
|
|
|
|
<objective>
|
|
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`.
|
|
</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/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
|
|
|
|
<interfaces>
|
|
<!-- Key types and contracts the executor needs. -->
|
|
|
|
From classify/rules.go:
|
|
```go
|
|
type Rule struct {
|
|
Protocol string
|
|
DstPort uint16
|
|
Class TrafficClass
|
|
}
|
|
var DefaultRules = []Rule{ ... } // 12 rules, first-match-wins
|
|
```
|
|
|
|
From classify/types.go:
|
|
```go
|
|
type TrafficClass string
|
|
func AllClasses() []TrafficClass // returns 14 built-in classes
|
|
```
|
|
|
|
From synth/config.go:
|
|
```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):
|
|
```go
|
|
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):
|
|
```go
|
|
func writeTOML(t *testing.T, content string) string // creates temp TOML file
|
|
// Tests call config.Load(path) and check returned map
|
|
```
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 1: RawRule, LoadResult, validation, conversion, and auto-freq with TDD</name>
|
|
<files>config/config.go, config/config_test.go</files>
|
|
<read_first>config/config.go, config/config_test.go, classify/rules.go, classify/types.go, synth/config.go</read_first>
|
|
<behavior>
|
|
- 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)
|
|
</behavior>
|
|
<action>
|
|
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):
|
|
```go
|
|
type RawRule struct {
|
|
Port *uint16 `toml:"port"`
|
|
Protocol string `toml:"protocol"`
|
|
Class string `toml:"class"`
|
|
}
|
|
```
|
|
|
|
2. Add Rules field to rawConfig:
|
|
```go
|
|
type rawConfig struct {
|
|
Sounds map[string]SoundOverride `toml:"sounds"`
|
|
Rules []RawRule `toml:"rules"`
|
|
}
|
|
```
|
|
|
|
3. Add LoadResult struct (per D-13, Claude's Discretion: struct over tuple):
|
|
```go
|
|
type LoadResult struct {
|
|
FreqCfgs map[classify.TrafficClass]synth.FreqConfig
|
|
UserRules []classify.Rule
|
|
ConfigPath string
|
|
}
|
|
```
|
|
|
|
4. Add validateRules function called from validate():
|
|
```go
|
|
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
|
|
}
|
|
```
|
|
|
|
5. Add convertRules function:
|
|
```go
|
|
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
|
|
}
|
|
```
|
|
|
|
6. Add autoAssignFreq function (per D-07/D-08, using FNV-32a):
|
|
```go
|
|
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
|
|
}
|
|
```
|
|
|
|
7. Add addAutoFreqEntries function (called AFTER merge, per Pitfall 4):
|
|
```go
|
|
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,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
8. Change Load() signature to return LoadResult:
|
|
```go
|
|
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]].
|
|
|
|
9. 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).
|
|
</action>
|
|
<verify>
|
|
<automated>go test ./config/... -v -count=1</automated>
|
|
</verify>
|
|
<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>
|
|
<done>
|
|
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.
|
|
</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `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
|
|
</verification>
|
|
|
|
<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>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/07-custom-rules-and-print-config/07-01-SUMMARY.md`
|
|
</output>
|