This document supersedes the pre-implementation v1.0 architecture research. It is grounded in the actual codebase (3,254 lines, 6 packages) and answers: what changes, what's new, and in what order.
1.**TOML config file** — override frequencies and waveforms per built-in class
2.**Additional waveforms** — square, sawtooth, triangle alongside existing sine
3.**User-defined classification rules** — TOML-defined rules prepended before DefaultRules
---
## Integration Point Analysis
### Feature 1: TOML Config File
**Where config is consumed today:**`synth/config.go` holds a package-level `var ClassFreqConfigs`. `synth/bank.go:NewBank()` reads it directly with `ClassFreqConfigs[class]`. No config is passed through `encode.RunSynthesis` or `main.go`.
**Required change:**`NewBank` must accept a config parameter instead of reading the global. `encode.RunSynthesis` must accept and forward a config. `main.go` must load config from disk and pass it in.
**New package: `config/`**
This package does not exist yet in the codebase (the pre-implementation research anticipated it but it was deferred). It should own:
- TOML struct definitions
- File discovery logic (auto-detect `./netsynth.toml`, then `~/.config/netsynth/config.toml`)
- Merging: loaded config overlays defaults, does not replace them entirely
**TOML library:** Use `github.com/BurntSushi/toml`. It is the de-facto standard for TOML in Go (used by Hugo, dep, buf, etc.). Already a transitive dependency in many Go module graphs. Provides struct-tag-based decode, good error messages.
**Where waveform logic lives today:**`synth/oscillator.go:Advance()` — pure sine via `math.Sin`. The `HarmonicDef.Ratio` and `HarmonicDef.Amplitude` fields are stored in `FreqConfig.Harmonics` but the waveform function is hardcoded.
**Required change:**`Oscillator.Advance` must dispatch on a waveform type. Two clean approaches:
**Option A (recommended): Waveform enum on Oscillator**
Add a `waveform` field to `Oscillator`. `Advance` switches on it. `NewOscillator` gains a waveform parameter.
**What does NOT change:**`HarmonicDef`, `EMAAlpha`, `Layer.UpdateTarget`, `Layer.AdvanceSample`, `OscillatorBank.RenderWindow`, `mixer.go`, `encode/mp3.go`. The waveform change is contained to `oscillator.go` and the `FreqConfig` struct.
---
### Feature 3: User-Defined Classification Rules
**Where rules are wired today:**`main.go` lines 111, 175 — both `runLiveMode` and `runPcapMode` call `classify.NewClassifier(classify.DefaultRules)` directly. No config is passed.
**Required change:** User rules from TOML prepend before `DefaultRules`. `Classifier` already supports arbitrary `[]Rule` — `NewClassifier(rules []Rule)` is the constructor. No change to `classifier.go` itself.
**New `TrafficClass` values:** User-defined classes in TOML produce new `TrafficClass` string values (e.g., `"myservice"`). `AllClasses()` in `classify/types.go` is currently a hardcoded slice. For user-defined classes, `AllClasses()` cannot be the source of truth for bank layer construction. `NewBank` must instead iterate over whatever classes have a `FreqConfig` entry.
This is a critical integration point: `bank.go:NewBank` currently ranges over `classify.AllClasses()`. If user classes can appear, `NewBank` must accept the full config map and range over that instead.
| `synth/oscillator.go` | Add `Waveform` type + `waveform` field; dispatch in `Advance` | Self-contained; no caller signature breaks except `NewOscillator` |
| `synth/config.go` | Add `Waveform Waveform` field to `FreqConfig`; default to `WaveformSine` | Requires `NewLayer` to pass waveform to `NewOscillator` |
| `synth/bank.go` | Accept `map[classify.TrafficClass]FreqConfig` param instead of reading global; range over param keys not `AllClasses()` | Decouples bank from global; enables user classes |
| `encode/mp3.go` | Accept `*config.Config` or merged `FreqConfig` map; pass to `NewBank` | Thin forwarding change |
| `cmd/netsynth/main.go` | Add `--config` flag; load config; prepend user rules; pass config to `RunSynthesis` | Touches both `runLiveMode` and `runPcapMode` |
| `classify/rules.go` | No change — `DefaultRules` stays as the fallback | Unchanged |
No external dependencies. Pure math. Testable with golden-sample unit tests (square wave sample at phase 0.25 should be 1.0, etc.). Does not affect `Layer`, `Bank`, or `encode` yet.
Add `Waveform` to `FreqConfig`. Update `NewLayer` to pass it to `NewOscillator`. `ClassFreqConfigs` entries default to `WaveformSine` (zero value — valid if `WaveformSine = 0`).
Change `NewBank(tau float64)` to `NewBank(tau float64, cfgs map[classify.TrafficClass]FreqConfig)`. Update `encode/mp3.go:RunSynthesis` to pass `synth.ClassFreqConfigs` as default.
At this point the system is functionally identical to v1.0 but `NewBank` no longer reads a global.
Implement `config.Load()`, file discovery, and the `DefaultConfig()` function that wraps `synth.ClassFreqConfigs`. No TOML parsing yet — start with the struct definitions and the merge logic.
### Step 5: `--config` flag and user rule merging in `main.go`
Wire `config.Load()` into `run()`. Pass user rules to both `runLiveMode` and `runPcapMode`. Pass merged `FreqConfig` map to `RunSynthesis`.
At this point a minimal TOML config (empty file, or `[[rule]]` only) can be validated end-to-end.
**Files changed:**`cmd/netsynth/main.go`.
### Step 6: Custom frequency and waveform overrides in config
Implement the `[[class]]` TOML section parsing. Add `FreqConfigs()` method to `Config` that returns the merged map (user overrides applied over defaults). Write table-driven tests: "TOML sets HTTPS to 200 Hz sawtooth, bank layer for HTTPS uses 200 Hz sawtooth."
**Files changed:**`config/config.go`.
### Step 7: User-defined classes end-to-end
Support `[[class]]` entries with names not in `classify.AllClasses()`. These become new `TrafficClass` values. User `[[rule]]` entries pointing to these classes are prepended to `DefaultRules`. The bank creates layers for all classes in the merged `FreqConfig` map.
This step requires the most cross-package coordination but by this point each piece is already in place.
**Files changed:**`config/config.go`, `cmd/netsynth/main.go` (verification that unknown class names don't panic).
---
## Component Boundaries After v1.1
| Component | Responsibility | Communicates With |
| `synth/oscillator` | Phase-accumulator for sine/square/sawtooth/triangle | Used by `Layer` |
| `synth/bank` | Accepts freq config map, constructs one `Layer` per entry | `encode` passes config map in |
| `encode` | Receives config map from `main`, passes to `NewBank` | Thin pass-through |
| `cmd/netsynth/main` | Loads config, merges rules, wires all stages | All packages |
| `classify` | Rules engine (unchanged); `DefaultRules` stays as package-level var | `main` constructs with merged rules |
---
## Critical Integration Constraints
### `AllClasses()` Is Not the Source of Truth for Bank Construction
`bank.go` currently iterates `classify.AllClasses()` to construct layers. After v1.1, the bank must iterate the keys of the `FreqConfig` map passed to it. User-defined classes will not appear in `AllClasses()`. If this is not changed, user-defined class packets will be aggregated in `WindowSnapshot.Counts` but have no corresponding layer — they will produce silence and no error.
**Fix:**`NewBank` iterates `maps.Keys(cfgs)` (or equivalent range over the map), not `classify.AllClasses()`.
### Class Name Validation Must Happen at Config Load Time
If a `[[rule]]` references a class name that has no corresponding `[[class]]` entry and is not a builtin, the system will silently mis-classify packets into a layer that doesn't exist. Validate at `config.Load()` time: every class name in `[[rule]]` must resolve to either a builtin `TrafficClass` or a `[[class]]` entry in the same config.
### `encode.RunSynthesis` Signature Change Is a Breaking API Change
`encode.RunSynthesis(snapshots []classify.WindowSnapshot, outputPath string)` will need to accept the config. If any external code (tests, future callers) uses this signature, they will break. Keep the change to a single place and update all call sites in the same commit.
---
## Anti-Patterns to Avoid
### Anti-Pattern: Reading Global `ClassFreqConfigs` from Multiple Places
If `NewBank`, `encode.RunSynthesis`, and config loading all reference the package-level `synth.ClassFreqConfigs`, the merge point becomes ambiguous. The fix (Step 3 above) centralizes config reading to one place: `config.DefaultConfig()` reads from `ClassFreqConfigs` once when building defaults; everything downstream receives the already-merged map.
### Anti-Pattern: Storing `Waveform` as a String Everywhere
Keeping `waveform` as a `string` from TOML all the way into `Oscillator` means every advance call parses or switches on a string. Parse the string to a `Waveform` int type at config-load time. The `Oscillator` field should be a typed `Waveform`, not `string`.
### Anti-Pattern: User Rules Appended After DefaultRules
User rules must **prepend**`DefaultRules`, not append. `DefaultRules` ends with catch-all rules (`DstPort: 0`) that match any TCP or UDP packet. Appending user rules after these catch-alls means they will never be reached.