Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
29 KiB
Phase 6: Config Package and Sound Overrides - Research
Researched: 2026-03-26 Domain: Go TOML config loading, partial merge semantics, CLI flag wiring Confidence: HIGH
<user_constraints>
User Constraints (from CONTEXT.md)
Locked Decisions
- D-01: Use keyed TOML tables
[sounds.<classname>]for per-class overrides. Each table supportsfrequency(float64, Hz) andwaveform(string: "sine", "square", "sawtooth", "triangle"). Class names matchclassify.TrafficClassstring values (e.g.,[sounds.ICMP],[sounds.HTTPS]). - D-02: Top-level structure is flat — no deeply nested hierarchies. Future phases (custom rules) will add
[[rules]]array-of-tables at the top level. - D-03: Per-field overlay merge — only fields explicitly set in TOML override defaults. Unspecified fields retain their built-in values. For example, setting only
frequencyfor ICMP leaves its waveform and harmonics unchanged. This satisfies CFG-04 (partial override without replicating entire config). - D-04: Merge produces a
map[classify.TrafficClass]FreqConfigthat is passed tosynth.NewBank()via the injection seam from Phase 5. The default map issynth.ClassFreqConfigs. - D-05: Discovery order (most-specific wins):
--config <path>>./netsynth.toml>~/.config/netsynth/config.toml. If--configis specified and the file does not exist, exit with a clear error before capture begins (CFG-03). If no config is found via auto-discovery, proceed silently with defaults (CFG-02). - D-06: Only one config file is loaded — no multi-file merge. The first found in precedence order wins entirely.
- D-07: Unknown keys cause an immediate startup error naming the unrecognized key (CFG-05). Use TOML strict decoding to detect unknown keys. Suggest the closest valid key name if edit distance is small (nice-to-have, Claude's discretion on implementation).
- D-08: Type mismatches (e.g.,
frequency = "not a number") produce a clear error with field name and expected type, before capture begins. - D-09: Unknown class names in
[sounds.<classname>]produce a warning (not error) — this prepares for Phase 7 where user-defined class names are valid. - D-10:
encode.RunSynthesissignature changes to accept the merged config map (or loads config internally). The--configflag is added to the Cobra root command incmd/netsynth/main.go. - D-11: Config loading happens once at startup, before any capture begins — fail fast on all config errors.
Claude's Discretion
- TOML library choice (BurntSushi/toml vs pelletier/go-toml) — researcher should evaluate both
- Whether to create a dedicated
configpackage or keep loading incmd/netsynth - Waveform string-to-WaveformType mapping implementation details
- Edit distance algorithm for typo suggestions (or skip if complexity isn't justified)
Deferred Ideas (OUT OF SCOPE)
--print-configcommand (CFG-06) — scoped to Phase 7- Custom classification rules (
[[rules]]TOML blocks) — scoped to Phase 7 - Config hot-reload — explicitly out of scope per REQUIREMENTS.md
</user_constraints>
<phase_requirements>
Phase Requirements
| ID | Description | Research Support |
|---|---|---|
| CFG-01 | User can create a TOML config file that overrides default sound mappings | [sounds.<classname>] table pattern decodes into map[string]SoundOverride; per-field merge into synth.ClassFreqConfigs clone |
| CFG-02 | Tool auto-discovers config from ./netsynth.toml or ~/.config/netsynth/config.toml (silent if absent) |
os.Stat probe + os.UserConfigDir() for XDG path; errors.Is(err, fs.ErrNotExist) for silent miss |
| CFG-03 | User can specify an explicit config path via --config flag (error if file missing) |
Cobra StringVar flag; fail-fast os.Stat check returns error before capture begins |
| CFG-04 | User can override individual values without replicating the entire default config (partial override) | Pointer fields (*float64, *string) in the TOML decode struct allow distinguishing "explicitly zero" from "not set"; overlay merge copies only non-nil fields |
| CFG-05 | Unknown keys in config file produce a clear error with the typo'd key name | BurntSushi/toml MetaData.Undecoded() returns unmatched keys after decode; format as error message |
</phase_requirements>
Summary
Phase 6 adds a config package responsible for loading a TOML config file, validating it, and merging it over the synth.ClassFreqConfigs default map. The merge output is a map[classify.TrafficClass]synth.FreqConfig that is handed to synth.NewBank() — the injection seam already exists from Phase 5.
The core technical challenge is partial override semantics: a user who sets only frequency for ICMP must not accidentally clear its waveform. This requires the decode struct to use pointer fields (*float64, *string) so that absent keys remain nil at decode time. The merge loop then only copies non-nil values over the defaults.
Unknown-key detection uses BurntSushi/toml v1.6.0's MetaData.Undecoded() method, which is reliable because it operates on the actual set of keys the parser traversed. The alternative (pelletier/go-toml v2.3.0's DisallowUnknownFields) is also viable but adds a dependency with a different API surface and returns human-formatted error strings rather than structured key lists — less useful for the "suggest closest valid key" nice-to-have.
Primary recommendation: Use github.com/BurntSushi/toml v1.6.0. Use a dedicated config package. Implement partial merge with pointer fields in the TOML decode struct.
Standard Stack
Core
| Library | Version | Purpose | Why Standard |
|---|---|---|---|
github.com/BurntSushi/toml |
v1.6.0 | TOML parsing and MetaData for unknown-key detection | Simpler API than pelletier v2; Undecoded() returns structured []Key (not formatted error strings); DecodeFile() is a one-liner; v1.6.0 published December 2025 |
Supporting
| Library | Version | Purpose | When to Use |
|---|---|---|---|
os (stdlib) |
Go 1.24 | File existence checks, UserConfigDir() for XDG path |
Always — no external dependency needed for discovery logic |
errors/fs (stdlib) |
Go 1.24 | errors.Is(err, fs.ErrNotExist) for silent-miss on auto-discovery |
Always |
Alternatives Considered
| Instead of | Could Use | Tradeoff |
|---|---|---|
BurntSushi/toml v1.6.0 |
pelletier/go-toml v2.3.0 |
go-toml has DisallowUnknownFields() built-in (cleaner API) but returns StrictMissingError with a formatted string — harder to extract just the key name for a "did you mean?" suggestion. BurntSushi returns []toml.Key which is structured. For this use case, BurntSushi is easier to work with. |
| Pointer fields for partial override | Separate "is-set" booleans | Pointer fields are idiomatic in Go for "optional" semantics. Booleans add field count and are error-prone. |
Dedicated config package |
Inline in cmd/netsynth |
A config package makes the loader independently testable without a Cobra dependency. Given the complexity (validation, merge, discovery), a separate package is justified. |
Installation:
go get github.com/BurntSushi/toml@v1.6.0
Version verification (confirmed 2026-03-26):
github.com/BurntSushi/toml v1.6.0 (December 18, 2025)
github.com/pelletier/go-toml/v2 v2.3.0 (March 24, 2026 — alternative)
Architecture Patterns
Recommended Project Structure
config/
├── config.go # Load(), Merge(), Validate() — public API
└── config_test.go # table-driven tests for all CFG requirements
The config package has one exported function signature the planner cares about:
// Load finds, parses, validates, and merges a config file.
// configPath is the --config flag value; empty string triggers auto-discovery.
// Returns the merged FreqConfig map (defaults + overrides) ready for synth.NewBank.
// Returns an error on: file-not-found when --config is explicit, parse errors,
// unknown keys, type mismatches. Returns no error (uses defaults) when no config
// is found during auto-discovery.
func Load(configPath string) (map[classify.TrafficClass]synth.FreqConfig, error)
Pattern 1: TOML Decode Struct with Pointer Fields
What: The TOML config file maps to a Go struct where every overridable field is a pointer. nil means "not set by user"; non-nil means "user explicitly specified this value."
When to use: Whenever you need to distinguish "field absent from config" from "field set to zero value" — mandatory for partial override semantics (CFG-04).
// Source: BurntSushi/toml documentation + partial-override pattern
// config/config.go
// SoundOverride holds optional per-class sound parameters from TOML.
// Pointer fields: nil = not set (keep default), non-nil = user override.
type SoundOverride struct {
Frequency *float64 `toml:"frequency"`
Waveform *string `toml:"waveform"`
}
// rawConfig is the top-level TOML decode target.
type rawConfig struct {
Sounds map[string]SoundOverride `toml:"sounds"`
}
Pattern 2: Unknown-Key Detection with MetaData.Undecoded()
What: After decoding, check md.Undecoded() for any keys in the TOML file that did not map to a field in the decode struct. Return an error naming the first unrecognized key.
When to use: Required for CFG-05. Also the mechanism to detect field-level typos within a [sounds.ICMP] block (e.g., frequncy vs frequency).
// Source: pkg.go.dev/github.com/BurntSushi/toml
// config/config.go
func parse(path string) (rawConfig, error) {
var raw rawConfig
md, err := toml.DecodeFile(path, &raw)
if err != nil {
return raw, fmt.Errorf("config parse error: %w", err)
}
if undecoded := md.Undecoded(); len(undecoded) > 0 {
// undecoded[0] is a toml.Key ([]string); join for human-readable path
keyPath := strings.Join(undecoded[0], ".")
return raw, fmt.Errorf("config: unknown key %q — check spelling", keyPath)
}
return raw, nil
}
IMPORTANT NOTE on nested map + Undecoded(): When the decode struct uses map[string]SoundOverride for [sounds], the TOML library cannot know what map keys are "valid" — all string keys are valid map keys. This means Undecoded() will NOT catch a misspelled class name like [sounds.ICMP_typo] (it IS decoded, just into a wrong map key). However, Undecoded() WILL catch field-level typos within a class block like [sounds.ICMP] with frequncy = 440 because frequncy doesn't match any SoundOverride field. Class-name validation is handled separately in the merge step (D-09: log a warning for unknown class names).
Pattern 3: Per-Field Overlay Merge
What: Iterate over the default ClassFreqConfigs map, copy it, then for each entry found in the TOML overrides, copy only the non-nil pointer fields into the working copy.
When to use: This is the CFG-04 implementation. Must run after parse and validation.
// config/config.go
func merge(
defaults map[classify.TrafficClass]synth.FreqConfig,
overrides map[string]SoundOverride,
) map[classify.TrafficClass]synth.FreqConfig {
// Deep-copy defaults
result := make(map[classify.TrafficClass]synth.FreqConfig, len(defaults))
for k, v := range defaults {
result[k] = v
}
for className, override := range overrides {
class := classify.TrafficClass(className)
cfg, known := result[class]
if !known {
// D-09: unknown class name = warning, not error (Phase 7 may define it)
fmt.Fprintf(os.Stderr, "Warning: config: unknown class %q (ignored)\n", className)
continue
}
if override.Frequency != nil {
cfg.BaseHz = *override.Frequency
// When frequency changes, regenerate harmonics if a waveform preset is active
if cfg.WaveformType != synth.WaveformCustom {
cfg.Harmonics = synth.WaveformPresetHarmonics(cfg.WaveformType, cfg.BaseHz, synth.SampleRate)
}
}
if override.Waveform != nil {
wt, err := parseWaveform(*override.Waveform)
if err != nil {
// Validation catches this before merge; this is a safety guard
continue
}
cfg.WaveformType = wt
cfg.Harmonics = synth.WaveformPresetHarmonics(wt, cfg.BaseHz, synth.SampleRate)
}
result[class] = cfg
}
return result
}
Pattern 4: Waveform String-to-Type Mapping
What: A simple switch converts the TOML waveform string to synth.WaveformType. Validation happens before merge.
// config/config.go
var validWaveforms = map[string]synth.WaveformType{
"sine": synth.WaveformSine,
"square": synth.WaveformSquare,
"sawtooth": synth.WaveformSawtooth,
"triangle": synth.WaveformTriangle,
}
func parseWaveform(s string) (synth.WaveformType, error) {
if wt, ok := validWaveforms[s]; ok {
return wt, nil
}
valid := []string{"sine", "square", "sawtooth", "triangle"}
return 0, fmt.Errorf("config: invalid waveform %q — valid values: %s", s, strings.Join(valid, ", "))
}
Pattern 5: Auto-Discovery with os.UserConfigDir
What: Check paths in precedence order. Return the path of the first file found, or "" (empty) if none found. Never log anything for a missing auto-discovered file.
// config/config.go
func discoverPath() string {
// 1. Working directory
if _, err := os.Stat("netsynth.toml"); err == nil {
return "netsynth.toml"
}
// 2. XDG config dir
dir, err := os.UserConfigDir()
if err != nil {
return ""
}
p := filepath.Join(dir, "netsynth", "config.toml")
if _, err := os.Stat(p); err == nil {
return p
}
return ""
}
os.UserConfigDir() returns $XDG_CONFIG_HOME or $HOME/.config on Linux (Go stdlib, no extra dependency). Confirmed by Go source: returns $XDG_CONFIG_HOME if set, else $HOME/.config on Unix.
Pattern 6: encode.RunSynthesis Signature Change
What: RunSynthesis currently calls synth.NewBank(1.0, synth.ClassFreqConfigs) hardcoded. Phase 6 changes the signature to accept the merged config map.
The simplest approach: pass the merged config map as a parameter (rather than loading config inside encode). This keeps encode unaware of config loading and makes testing easier.
// encode/mp3.go — updated signature
func RunSynthesis(
snapshots []classify.WindowSnapshot,
outputPath string,
freqCfgs map[classify.TrafficClass]synth.FreqConfig,
) error {
// ...
bank := synth.NewBank(1.0, freqCfgs) // was: synth.ClassFreqConfigs
// ...
}
Caller in cmd/netsynth/main.go passes the result of config.Load(configPath).
Anti-Patterns to Avoid
- Decode into
map[string]interface{}: Loses type safety, makes unknown-field detection harder, requires runtime type assertions. Use typed structs. - Load config inside
encodepackage: Couples audio encoding to config I/O; breaks test isolation. Config loading belongs incmd/netsynth/main.go(callsconfig.Load) or a dedicatedconfigpackage. - Validate waveform strings after merge: Validate before merging so the error is caught at startup (D-11), not silently ignored.
- Deep-copy using
=assignment on map values:synth.FreqConfigcontains a[]HarmonicDefslice; a simple struct copy shares the underlying array. Use an explicit copy of the slice if you mutateHarmonicsduring merge. (The merge code above reconstructs harmonics from the preset, so this is safe — but important to be aware of.)
Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---|---|---|---|
| TOML parsing | Custom parser | BurntSushi/toml v1.6.0 |
TOML 1.1 compliance, error messages, datetime support, tested at scale |
| Unknown-key detection | Post-parse key comparison | md.Undecoded() from BurntSushi |
Already built into the library; handles nested paths correctly |
| XDG config path | Manual $HOME/.config string concat |
os.UserConfigDir() stdlib |
Handles $XDG_CONFIG_HOME override correctly, platform-portable |
Key insight: The partial-override merge logic is the one piece that must be written from scratch — no library does "overlay a sparse map of optional overrides over a typed defaults map." But it is ~20 lines of straightforward Go.
Runtime State Inventory
Step 2.5 SKIPPED — this is a new feature addition, not a rename/refactor/migration phase. No runtime state is being renamed or migrated.
Common Pitfalls
Pitfall 1: Undecoded() Does Not Catch Unknown Class Names
What goes wrong: Developer assumes md.Undecoded() will catch [sounds.ICMP_TYPO] as an unknown key and provide CFG-05 coverage for class-name typos.
Why it happens: map[string]SoundOverride decodes any string as a valid map key — the TOML parser has no way to know which class names are valid. Undecoded() only catches keys that don't match ANY field (struct field name, map key, or slice element). Since all map keys are valid, no class name is "undecodeable."
How to avoid: Separate the two concerns. Field-level unknown keys (e.g., frequncy) ARE caught by Undecoded(). Class-name typos are caught in the merge step by checking whether classify.TrafficClass(className) exists in synth.ClassFreqConfigs. The CONTEXT.md decision D-09 says unknown class names produce a warning (not error) to allow for Phase 7 user-defined classes — so this is by design.
Warning signs: Test for both: write a test with [sounds.ICMP] containing frequncy = 440 (should error) AND a test with [sounds.ICMP_TYPO] containing frequency = 440 (should warn, not error).
Pitfall 2: Partial Override Accidentally Clears WaveformType
What goes wrong: User sets only frequency = 300 for ICMP. After merge, ICMP's WaveformType is reset to WaveformCustom because the merge loop creates a new FreqConfig{} instead of starting from the default.
Why it happens: Copy-by-value from defaults is skipped, or merge starts from a zero-value struct.
How to avoid: Always start the merge from the DEFAULT FreqConfig for that class. The merge loop copies defaults[class] first, then overlays only non-nil pointer fields.
Warning signs: Test case: set only frequency for a class with WaveformCustom — verify waveform field is unchanged. Test case: set only waveform for a class — verify frequency is unchanged.
Pitfall 3: Frequency Change Does Not Regenerate Harmonics for Preset Waveforms
What goes wrong: User sets frequency = 300 for HTTPS (which has WaveformCustom by default, so this is fine). But if a user sets frequency = 300 for a class that was previously configured with WaveformSine (via an earlier config entry), the harmonics may be stale from the old frequency.
Why it happens: synth.WaveformPresetHarmonics generates harmonics based on baseHz. If you update BaseHz without regenerating harmonics, the preset harmonics are anchored to the old frequency.
How to avoid: In the merge function: when updating Frequency, check if WaveformType != WaveformCustom. If true, regenerate Harmonics from the new frequency. The merge example above handles this correctly.
Warning signs: For the 14 built-in classes, all have WaveformCustom (hand-tuned harmonics), so this pitfall only bites if the user sets both waveform and frequency in two separate steps — or if a future phase pre-configures preset waveforms on built-ins.
Pitfall 4: --config File-Not-Found vs Auto-Discovery Silence
What goes wrong: When --config /path/to/missing.toml is specified, the code returns the same "no config found, using defaults" behavior as auto-discovery silence.
Why it happens: os.Stat errors are treated uniformly regardless of how the path was obtained.
How to avoid: In the Load function, branch on whether configPath was explicitly provided: if it was, a fs.ErrNotExist is a user error (return error); if it came from auto-discovery, fs.ErrNotExist is normal (return nil error, use defaults).
Warning signs: CFG-03 acceptance criterion explicitly tests this: explicit path must error, absent auto-discovery must be silent.
Pitfall 5: go.mod Tidy Drops TOML Dependency
What goes wrong: go mod tidy is run after adding BurntSushi/toml to go.mod but before any .go file in the module actually imports it. Tidy removes it.
Why it happens: go mod tidy removes unused dependencies.
How to avoid: Add the import in config/config.go before running go mod tidy.
Code Examples
Complete config.go Skeleton
// Source: BurntSushi/toml docs + project pattern
// config/config.go
package config
import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
"github.com/BurntSushi/toml"
"github.com/netsynth/netsynth/classify"
"github.com/netsynth/netsynth/synth"
)
type SoundOverride struct {
Frequency *float64 `toml:"frequency"`
Waveform *string `toml:"waveform"`
}
type rawConfig struct {
Sounds map[string]SoundOverride `toml:"sounds"`
}
// Load is the single public entry point.
// configPath: value of --config flag; empty = auto-discover.
func Load(configPath string) (map[classify.TrafficClass]synth.FreqConfig, error) {
path, explicit, err := resolvePath(configPath)
if err != nil {
return nil, err
}
if path == "" {
// No config found during auto-discovery — use defaults silently (CFG-02)
return copyDefaults(), nil
}
raw, err := parseFile(path)
if err != nil {
if explicit && errors.Is(err, fs.ErrNotExist) {
return nil, fmt.Errorf("config file not found: %s", path)
}
return nil, err
}
if err := validate(raw); err != nil {
return nil, err
}
return merge(copyDefaults(), raw.Sounds), nil
}
Example TOML Config File
# netsynth.toml — override ICMP and SSH sounds
[sounds.ICMP]
frequency = 80.0
waveform = "square"
[sounds.SSH]
frequency = 400.0
# waveform not set — SSH keeps its default waveform
Test Pattern (table-driven)
// config/config_test.go
func TestLoadPartialOverride(t *testing.T) {
// Write a temp TOML file with only frequency for ICMP
tomlContent := `
[sounds.ICMP]
frequency = 100.0
`
f, _ := os.CreateTemp(t.TempDir(), "*.toml")
f.WriteString(tomlContent)
f.Close()
cfgs, err := Load(f.Name())
if err != nil {
t.Fatalf("Load: %v", err)
}
// ICMP frequency overridden
if cfgs[classify.ClassICMP].BaseHz != 100.0 {
t.Errorf("ICMP BaseHz: got %v, want 100.0", cfgs[classify.ClassICMP].BaseHz)
}
// ICMP waveform unchanged (WaveformCustom = 0)
if cfgs[classify.ClassICMP].WaveformType != synth.WaveformCustom {
t.Errorf("ICMP WaveformType: got %v, want WaveformCustom", cfgs[classify.ClassICMP].WaveformType)
}
// DNS frequency unchanged
if cfgs[classify.ClassDNS].BaseHz != synth.ClassFreqConfigs[classify.ClassDNS].BaseHz {
t.Errorf("DNS BaseHz unexpectedly changed")
}
}
func TestLoadUnknownKey(t *testing.T) {
tomlContent := `
[sounds.ICMP]
frequncy = 440
`
f, _ := os.CreateTemp(t.TempDir(), "*.toml")
f.WriteString(tomlContent)
f.Close()
_, err := Load(f.Name())
if err == nil {
t.Fatal("expected error for unknown key 'frequncy', got nil")
}
if !strings.Contains(err.Error(), "frequncy") {
t.Errorf("error should name the bad key, got: %v", err)
}
}
State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|---|---|---|---|
google/gopacket |
gopacket/gopacket v1.5.0 |
2022-2024 | N/A for this phase |
| BurntSushi/toml v0.x | v1.6.0 (TOML 1.1 enabled by default) | December 2025 | TOML 1.1 compliance; API unchanged, same Decode/DecodeFile functions |
go-audio/generator |
ARCHIVED (Feb 2026, read-only) | February 2026 | Do not use; project already avoids it |
Current versions confirmed 2026-03-26:
BurntSushi/tomlv1.6.0 (December 18, 2025) — TOML 1.1 default, stable APIpelletier/go-toml/v2v2.3.0 (March 24, 2026) — alternative if structured error needed
Open Questions
-
Typo suggestion for unknown keys (D-07 nice-to-have)
- What we know: BurntSushi returns
[]toml.Key(structured), Levenshtein distance is ~15 lines of Go orgithub.com/agnivade/levenshtein(tiny, zero-dependency) - What's unclear: Is the complexity worth it for 2 valid field names per class block (
frequency,waveform)? - Recommendation: Skip the external library. Implement inline: for each undecoded key, if it has edit distance ≤ 2 from any valid key name, append " (did you mean: X?)" to the error. The valid key set for field names is small and static:
["frequency", "waveform"]. This is ~10 lines of Go.
- What we know: BurntSushi returns
-
copyDefaults() — shallow vs deep copy of Harmonics slices
- What we know:
synth.FreqConfig.Harmonicsis a[]HarmonicDef. Go'smap[K]Vassignment copies struct values (including slice headers) but the underlying array is shared. - What's unclear: Does this matter if merge only replaces the whole slice (via
WaveformPresetHarmonics) rather than appending to it? - Recommendation: Since the merge code assigns a freshly-generated
[]HarmonicDeffromWaveformPresetHarmonics(never mutates the original), shallow copy is safe. No deep copy needed. Document this in a comment for future maintainers.
- What we know:
Environment Availability
Step 2.6: This phase introduces one new external dependency:
| Dependency | Required By | Available | Version | Fallback |
|---|---|---|---|---|
github.com/BurntSushi/toml |
config.Load() TOML parsing | ✓ (fetched via go get) | v1.6.0 | pelletier/go-toml v2.3.0 |
os.UserConfigDir() |
Auto-discovery of ~/.config/netsynth/config.toml |
✓ (Go stdlib) | Go 1.13+ | N/A — stdlib |
| Go 1.24.1 toolchain | Module minimum | ✓ | 1.24.1 | N/A |
| C compiler (CGo) | go-lame MP3 encoding (pre-existing) | Assumed ✓ (Phase 2+ already requires this) | — | N/A |
No missing dependencies with no fallback. BurntSushi/toml confirmed fetchable from pkg.go.dev.
Validation Architecture
Test Framework
| Property | Value |
|---|---|
| Framework | Go standard testing package |
| Config file | None — go test ./... |
| Quick run command | go test ./config/... |
| Full suite command | go test ./... |
Phase Requirements → Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|---|---|---|---|---|
| CFG-01 | TOML overrides applied to correct class | unit | go test ./config/... -run TestLoadOverride |
Wave 0 |
| CFG-02 | No config file → silent, uses defaults | unit | go test ./config/... -run TestLoadNoConfig |
Wave 0 |
| CFG-03 | --config explicit path → error if missing |
unit | go test ./config/... -run TestLoadExplicitMissing |
Wave 0 |
| CFG-04 | Partial override: unset fields unchanged | unit | go test ./config/... -run TestLoadPartialOverride |
Wave 0 |
| CFG-05 | Unknown key → error naming the key | unit | go test ./config/... -run TestLoadUnknownKey |
Wave 0 |
| CFG-03 | --config flag wired in Cobra |
integration | go test ./cmd/netsynth/... -run TestConfigFlag |
Wave 0 |
Sampling Rate
- Per task commit:
go test ./config/... -count=1 - Per wave merge:
go test ./... -count=1 - Phase gate: Full suite green before
/gsd:verify-work
Wave 0 Gaps
config/config.go— package does not exist yet; create in Wave 1config/config_test.go— covers CFG-01 through CFG-05cmd/netsynth/main_test.go— addTestConfigFlagcovering CFG-03 CLI integration
(Existing test infrastructure covers all other packages; only config/ is new.)
Sources
Primary (HIGH confidence)
pkg.go.dev/github.com/BurntSushi/toml— v1.6.0 API:DecodeFile,MetaData.Undecoded(),[]Keytype; verified 2026-03-26- Go stdlib
os.UserConfigDir()— returns$XDG_CONFIG_HOMEor$HOME/.configon Linux; Go 1.13+ feature pkg.go.dev/github.com/pelletier/go-toml/v2— v2.3.0DisallowUnknownFields()/StrictMissingErrorAPI; verified 2026-03-26
Secondary (MEDIUM confidence)
- WebSearch: BurntSushi/toml Undecoded() approach verified against official GitHub source (
toml/decode.go) - WebSearch: pelletier/go-toml v2 DisallowUnknownFields verified against official docs
- WebSearch:
os.UserConfigDirXDG compliance — confirmed returns$XDG_CONFIG_HOMEor$HOME/.configon Linux per golang/go issue #29960
Tertiary (LOW confidence)
- WebSearch: edit distance typo suggestion libraries (agnivade/levenshtein, go-edlib) — not deeply evaluated; recommendation is inline 10-line implementation to avoid dependency
Metadata
Confidence breakdown:
- Standard stack: HIGH — versions confirmed via
go getlive fetch (v1.6.0 BurntSushi, v2.3.0 pelletier) - Architecture: HIGH — patterns derived from library documentation + existing codebase patterns
- Pitfalls: HIGH — Undecoded() + map key limitation is a documented behavior; partial-override via pointer fields is an established Go idiom
- TOML typo suggestion: LOW — nice-to-have from D-07; no deep investigation needed given small valid-key set
Research date: 2026-03-26 Valid until: 2026-06-26 (BurntSushi/toml is stable; go-toml v2 moves faster but is not the chosen library)