Files
yoloyolo/.planning/phases/07-custom-rules-and-print-config/07-RESEARCH.md
T
gurixandClaude Sonnet 4.6 e15972055d docs(07): research phase domain
Research for Phase 7 custom rules and print-config. Covers TOML array-of-tables parsing behavior, LoadResult struct design, FNV-32a auto-frequency algorithm, and print-config implementation patterns. All verified against existing source code.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 21:33:11 +01:00

28 KiB
Raw Blame History

Phase 7: Custom Rules and Print-Config - Research

Researched: 2026-03-26 Domain: Go config parsing (BurntSushi/toml), rule system extension, TOML serialization Confidence: HIGH

Summary

Phase 7 adds two related features: user-defined TOML classification rules that prepend before built-in DefaultRules, and a --print-config flag that serializes the full effective config to stdout as commented TOML without starting a capture. Both features are pure Go additions with no new dependencies — the entire implementation works within the existing stack.

The rule parsing extension is straightforward: add a Rules []RawRule field to rawConfig, validate required fields (protocol, class), and prepend the parsed []classify.Rule slice before classify.DefaultRules in main.go. BurntSushi/toml's Undecoded() mechanism already catches typos in [[rules]] blocks (verified experimentally — unknown fields in array-of-table entries appear in Undecoded() as rules.field_name). Auto-frequency assignment for new class names uses FNV-32a hash of the class name mapped to a 12002400 Hz range (above all 14 built-in frequencies which top out at 1047 Hz), producing deterministic and collision-resistant results.

The --print-config implementation has two design paths: Cobra flag on the root command (simpler, consistent with the existing flag-on-root pattern) or a Cobra subcommand. The flag path is recommended as it mirrors how --list-interfaces works. The output format uses manual string building (not the TOML encoder) to support # default / # override annotations that the encoder cannot produce.

Primary recommendation: Extend config.Load() to return a LoadResult struct (freqCfgs + user rules), prepend user rules in main.go, use FNV-32a for auto-frequency assignment, and implement --print-config as a flag that triggers early-exit in the run() function.

<user_constraints>

User Constraints (from CONTEXT.md)

Locked Decisions

  • D-01: Custom rules use TOML array-of-tables [[rules]] with three fields: port (uint16, optional — omit to match any port), protocol (string, required — "tcp", "udp", or "icmp"), and class (string, required — the TrafficClass name). Sound configuration for the class goes in a separate [sounds.<class>] block.
  • D-02: Port is optional. When omitted (or 0), the rule matches all traffic for the given protocol, mirroring the existing Rule.DstPort = 0 semantics in classify.DefaultRules.
  • D-03: Protocol is required. No implicit "match both TCP and UDP" behavior. User must write separate rules for each protocol.
  • D-04: User-defined rules are prepended before built-in DefaultRules (RULE-02). First-match-wins semantics are preserved. A user rule for port 443/tcp fires before the built-in HTTPS rule.
  • D-05: Rules within the TOML [[rules]] array maintain their file order. First rule in the file is first to match.
  • D-06: User-defined class names that match built-in names (e.g., class = "HTTPS") are treated as overrides, not errors. The user's rule fires first (prepended), so traffic matching it gets classified under the same built-in class name via the user rule. Sound config in [sounds.HTTPS] still applies.
  • D-07: New class names that have no [sounds.<class>] entry automatically get sensible defaults: a frequency from an unused range and sine waveform. This satisfies RULE-03 (no silent gaps for user-defined classes).
  • D-08: (Claude's Discretion) The auto-assignment algorithm — how to pick frequencies for new classes that don't collide with built-in frequencies. Could use a hash of the class name, a sequential pool, or a deterministic spread across an unused frequency band.
  • D-09: --print-config outputs the full effective config (defaults merged with user overrides and custom rules) as commented TOML. Comments indicate which values are defaults vs overrides. This satisfies CFG-06.
  • D-10: Output goes to stdout (pipeable). User can do netsynth --print-config > template.toml to create a config template. The command exits without starting a capture.
  • D-11: If a config file is loaded (via auto-discovery or --config), show its source path in a header comment.
  • D-12: The existing config.Load() function must be extended to parse [[rules]] blocks in addition to [sounds.*]. The rawConfig struct gains a Rules []RawRule field.
  • D-13: config.Load() returns both the merged FreqConfig map and the user rules (as []classify.Rule). The caller prepends user rules before classify.DefaultRules.

Claude's Discretion

  • How to extend rawConfig struct and Load() return type (tuple, struct, or new function)
  • Auto-frequency assignment algorithm for custom classes without explicit sound config
  • Whether --print-config is a Cobra subcommand or a flag on the root command
  • How to format the commented TOML output (manual string building vs TOML encoder + post-processing)

Deferred Ideas (OUT OF SCOPE)

None — discussion stayed within phase scope. </user_constraints>

<phase_requirements>

Phase Requirements

ID Description Research Support
RULE-01 User can define custom classification rules in TOML (match by port and/or protocol, assign class name and sound) D-01 through D-05; [[rules]] TOML parsing with BurntSushi/toml confirmed working
RULE-02 User-defined rules take priority over built-in rules (prepend before defaults) D-04; classify.NewClassifier(rules []Rule) already accepts any rule slice; prepend in main.go
RULE-03 User-defined class names automatically get a synthesis layer (no silent gaps) D-07/D-08; FNV-32a auto-frequency in 12002400 Hz range; synth.NewBank already accepts arbitrary class maps
CFG-06 User can run netsynth --print-config to see the effective config as commented TOML D-09 through D-11; flag on root command triggering early-exit; manual string building for comment annotations
</phase_requirements>

Standard Stack

No new dependencies required. All features use existing libraries.

Core (existing, no changes needed)

Library Version Purpose Notes
github.com/BurntSushi/toml v1.6.0 TOML decode + Undecoded() typo detection Handles [[rules]] array-of-tables natively; Undecoded() catches typos in rule blocks
github.com/spf13/cobra v1.10.2 --print-config flag addition PersistentPreRunE / early-exit pattern already used; add flag to root command
hash/fnv stdlib FNV-32a hash for auto-frequency assignment No import needed — already in Go stdlib
fmt stdlib Manual TOML comment string building for print-config Simplest approach for annotated output

Installation: No new packages. go build with existing go.mod is sufficient.

Architecture Patterns

config/
├── config.go        — extend rawConfig + Load() return type
└── config_test.go   — add tests for [[rules]] parsing, validate, auto-freq

cmd/netsynth/
└── main.go          — --print-config flag, prepend user rules, printConfig()

Pattern 1: rawConfig + Load() Return Type Extension

What: Add Rules []RawRule to rawConfig. Change Load() to return a LoadResult struct instead of a bare map.

When to use: Prefer a struct return over a tuple (map, []Rule, error) — Go tuples with 3+ values become unwieldy at the call site.

Recommended struct:

// Source: internal design — no external library required
type LoadResult struct {
    FreqCfgs  map[classify.TrafficClass]synth.FreqConfig
    UserRules []classify.Rule
    ConfigPath string // "" if no config loaded (for --print-config header comment)
}

type RawRule struct {
    Port     *uint16 `toml:"port"`
    Protocol string  `toml:"protocol"`
    Class    string  `toml:"class"`
}

type rawConfig struct {
    Sounds map[string]SoundOverride `toml:"sounds"`
    Rules  []RawRule                `toml:"rules"`
}

Call site in main.go:

result, err := config.Load(configPath)
if err != nil {
    return err
}
allRules := append(result.UserRules, classify.DefaultRules...)
classifier := classify.NewClassifier(allRules)

Pattern 2: RawRule Validation

What: Validate each RawRule before converting to classify.Rule. Required fields: protocol (non-empty, must be "tcp"/"udp"/"icmp"). class must be non-empty. port is optional.

Port omission semantics: In TOML, a missing port key means the struct field stays at its zero value. Using *uint16 (pointer) lets us distinguish "omitted" from "port = 0". In practice, both map to DstPort: 0 (match-any-port), so a uint16 field (non-pointer) also works here — the distinction is only meaningful for validation messages. Use *uint16 to match D-02 intent and for consistency with SoundOverride pointer fields.

Validation function:

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
}

Conversion to classify.Rule:

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
}

Pattern 3: Auto-Frequency Assignment via FNV-32a

What: For custom class names with no [sounds.<class>] block, assign a frequency deterministically from the class name using FNV-32a hash. Map to 12002400 Hz in 50 Hz steps.

Why FNV-32a: Fast, deterministic, already in stdlib, zero collisions observed across realistic class names. The 12002400 Hz range is entirely above the highest built-in frequency (1047 Hz for ClassUnknown4), so no overlap is possible.

Verified with test: The 24-step spread (1200, 1250, ..., 2350 Hz) gives clean frequency assignments: "MyApp"→1500 Hz, "GameServer"→1800 Hz, "MediaStream"→2150 Hz, "VoIP"→1750 Hz, "Database"→2000 Hz.

// Source: stdlib hash/fnv — no import required beyond "hash/fnv"
import "hash/fnv"

func autoAssignFreq(className string) float64 {
    h := fnv.New32a()
    h.Write([]byte(className))
    const (
        baseHz  = 1200.0
        stepHz  = 50.0
        numSteps = 24 // range: 12002350 Hz
    )
    step := h.Sum32() % numSteps
    return baseHz + float64(step)*stepHz
}

Integration point in merge(): After processing all Sounds overrides, iterate user rules. For each rule whose Class is not in the defaults map and has no [sounds.<class>] entry, call autoAssignFreq and create a FreqConfig with WaveformSine.

// In config.merge() or a new mergeCustomClasses() helper:
func addAutoFreqEntries(
    cfgs map[classify.TrafficClass]synth.FreqConfig,
    userRules []classify.Rule,
) {
    for _, rule := range userRules {
        class := rule.Class
        if _, exists := cfgs[class]; !exists {
            baseHz := autoAssignFreq(string(class))
            cfgs[class] = synth.FreqConfig{
                BaseHz:       baseHz,
                WaveformType: synth.WaveformSine,
                Harmonics:    synth.WaveformPresetHarmonics(synth.WaveformSine, baseHz, synth.SampleRate),
                Pan:          0.0,
            }
        }
    }
}

Pattern 4: --print-config as Flag on Root Command

What: Add --print-config bool flag. In run(), check the flag early and call a printConfig() function that writes to stdout, then return nil without starting a capture.

Why flag over subcommand: Consistent with --list-interfaces (existing pattern). Both are "inspect mode" flags that short-circuit the main capture path. Subcommand would require the user to write netsynth print-config rather than netsynth --print-config, deviating from the established CLI style.

// In main():
var printConfigFlag bool
rootCmd.Flags().BoolVar(&printConfigFlag, "print-config", false, "Print effective config as commented TOML and exit")

// In run():
if printConfigFlag {
    return runPrintConfig(configPath)
}

runPrintConfig() structure:

func runPrintConfig(configPath string) error {
    result, err := config.Load(configPath)
    if err != nil {
        return err
    }
    output, err := config.PrintConfig(result)
    if err != nil {
        return err
    }
    fmt.Print(output)
    return nil
}

Pattern 5: PrintConfig Output Format

What: Manual string building using fmt.Fprintf to a strings.Builder. The BurntSushi/toml encoder cannot add comments, so manual building is the correct approach.

Output structure:

# NetSynth effective configuration
# Config source: /home/user/.config/netsynth/config.toml
# Generated: 2026-03-26

# Custom classification rules (prepended before built-in rules)
# [[rules]]
# port = 8080
# protocol = "tcp"
# class = "MyApp"

[sounds]

  # ICMP — 65.0 Hz (default)
  [sounds.ICMP]
  frequency = 65.0
  waveform = "custom"

  # HTTPS — 300.0 Hz (override)
  [sounds.HTTPS]
  frequency = 300.0
  waveform = "sine"

Comment semantics:

  • # (default) — value came from synth.ClassFreqConfigs
  • # (override) — value was set in the user's config file
  • # (auto-assigned) — frequency was auto-generated for a user-defined class

Key implementation note: Custom class entries added by addAutoFreqEntries() need their origin tracked. The LoadResult or a separate annotation map needs to carry which classes are auto-assigned vs user-overridden vs defaults. Simplest approach: PrintConfig() receives the LoadResult and compares against synth.ClassFreqConfigs to determine annotation.

Pattern 6: Undecoded() and rules Interaction

Verified behavior (experimental): BurntSushi/toml's Undecoded() correctly catches unknown fields in [[rules]] blocks. A typo like typo_field = "bad" in a [[rules]] entry appears in Undecoded() as rules.typo_field. The existing parseFile() logic handles this automatically — no changes to the undecoded key check are needed.

Important: Class-name typos in [sounds.<class>] are STILL not caught (pre-existing limitation documented in the existing code comment). This is unchanged behavior for Phase 7.

Anti-Patterns to Avoid

  • Returning a tuple (map, []Rule, error) from Load(): Three-value tuples at call sites are verbose and error-prone. Use a LoadResult struct.
  • Using the TOML encoder for print-config output: The encoder cannot add # (default) comments. Manual fmt.Fprintf to strings.Builder is the correct approach.
  • Modifying classify.DefaultRules in place: Always prepend user rules as a new slice. DefaultRules is a package-level var that must not be mutated. Use append(userRules, classify.DefaultRules...) to create a fresh slice.
  • Silent auto-frequency collision: If two user-defined classes hash to the same frequency step, they will produce the same tone. This is acceptable for v1.1 (the probability is low with 24 steps) but document it in comments.
  • Placing --print-config check after config validation: The check should occur early in run() — right after config load, before any interface or output validation. The user should be able to run netsynth --print-config without specifying -i.

Don't Hand-Roll

Problem Don't Build Use Instead Why
TOML parsing with typo detection Custom parser BurntSushi/toml Undecoded() Already used; handles [[rules]] natively
Frequency hash Custom hash function stdlib hash/fnv FNV-32a Zero-dependency, deterministic, already stdlib
CLI flag parsing Manual arg parsing Cobra flag registration Consistent with all existing flags

Key insight: Every mechanism needed for Phase 7 already exists in the codebase. The risk is over-engineering: the entire implementation is struct extension + prepend + string building.

Common Pitfalls

Pitfall 1: Mutating classify.DefaultRules

What goes wrong: append(classify.DefaultRules, userRules...) prepends to the wrong end and may mutate the backing array of DefaultRules if the slice has capacity. Why it happens: Go slice append behavior with shared backing arrays. How to avoid: Always build the combined slice as append(userRules, classify.DefaultRules...) — user rules first, then defaults. This also gives the correct prepend order (D-04). Warning signs: Built-in rules fire before user rules for the same port/protocol.

Pitfall 2: --print-config Requires -i (incorrect)

What goes wrong: The run() function checks for -i / --read before checking --print-config, causing netsynth --print-config to fail with "interface required". Why it happens: The interface-required validation runs before the print-config check. How to avoid: Check printConfigFlag at the very top of run(), before the interface validation block. This matches how listIfaces is handled. Warning signs: netsynth --print-config returns "interface required" error instead of config output.

Pitfall 3: Undecoded() Reports sounds..frequency as Unknown

What goes wrong: After adding Rules []RawRule to rawConfig, the Undecoded() check may report sounds.MyApp.frequency as unknown if the SoundOverride struct is not correctly decoded. Why it happens: This was a concern during research but was verified NOT to occur — [sounds.MyApp] with a SoundOverride struct value decodes correctly alongside [[rules]]. No issue exists. How to avoid: N/A — verified working. Document in code that both sections coexist correctly.

Pitfall 4: Auto-Frequency Called for Built-in Class Names

What goes wrong: If a user writes class = "HTTPS" in [[rules]], addAutoFreqEntries() must not overwrite the existing HTTPS entry with an auto-generated frequency. Why it happens: The auto-assign loop checks if _, exists := cfgs[class]; !exists — built-in classes ARE in the defaults map, so this guard works correctly. But only if addAutoFreqEntries() runs AFTER merge() has already applied [sounds.*] overrides. How to avoid: Call addAutoFreqEntries() as the last step in the merge pipeline, after merge(defaults, raw.Sounds). The class-exists check then correctly skips both built-in and user-overridden classes.

Pitfall 5: print-config Missing User Rules Section

What goes wrong: printConfig() shows [sounds.*] entries but omits [[rules]] entries, making the output not round-trippable. Why it happens: Developer focuses on the sounds section (the existing config domain) and forgets rules. How to avoid: The LoadResult must include both UserRules []classify.Rule and ConfigPath string. The PrintConfig() function must emit the [[rules]] section before the [sounds.*] section, using the user rule slice.

Pitfall 6: validate() Must Run Before convertRules()

What goes wrong: An empty protocol or class field in [[rules]] gets silently converted to a classify.Rule with empty strings, producing confusing runtime behavior. Why it happens: convertRules() has no validation; it just copies fields. How to avoid: Call validateRules(raw.Rules) inside validate() (the existing validation entry point), before conversion. Fail fast at startup.

Code Examples

TOML File with Custom Rules (user-facing format)

# Classify custom app traffic on port 8080
[[rules]]
port = 8080
protocol = "tcp"
class = "MyApp"

# Match all UDP traffic to a custom class (port omitted = match any)
[[rules]]
protocol = "udp"
class = "AllUDP"

# Give MyApp a custom sound
[sounds.MyApp]
frequency = 300.0
waveform = "sine"

# Override built-in HTTPS sound
[sounds.HTTPS]
frequency = 400.0

rawConfig Extension

// config/config.go
type RawRule struct {
    Port     *uint16 `toml:"port"`
    Protocol string  `toml:"protocol"`
    Class    string  `toml:"class"`
}

type rawConfig struct {
    Sounds map[string]SoundOverride `toml:"sounds"`
    Rules  []RawRule                `toml:"rules"`
}

LoadResult Struct (replaces bare map return)

type LoadResult struct {
    FreqCfgs   map[classify.TrafficClass]synth.FreqConfig
    UserRules  []classify.Rule
    ConfigPath string // populated path or "" if auto-discovery found nothing
}

func Load(configPath string) (LoadResult, error) { ... }

Prepend User Rules in main.go

result, err := config.Load(configPath)
if err != nil {
    return err
}
// D-04: user rules prepend before built-ins; first-match-wins
allRules := append(result.UserRules, classify.DefaultRules...)
classifier := classify.NewClassifier(allRules)

FNV-32a Auto-Frequency

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
}

State of the Art

No changes to the underlying technology stack. All patterns are internal to the codebase.

Old Behavior New Behavior When Changed Impact
config.Load() returns map[classify.TrafficClass]synth.FreqConfig Returns LoadResult struct with FreqCfgs + UserRules + ConfigPath Phase 7 All callers of Load() need update (currently 1 call site: main.go:run())
Unknown class names in [sounds.*] silently ignored (warning) Still ignored with warning, but user-defined class names from [[rules]] get auto-freq entries instead Phase 7 New behavior for Phase 7 class names; old warning behavior preserved for truly unknown names
merge() only processes sound overrides merge() + addAutoFreqEntries() also handles new class synthesis entries Phase 7 Synthesis bank grows dynamically with user-defined classes

Environment Availability

Step 2.6: SKIPPED (no external dependencies identified — Phase 7 is a pure Go code extension using existing stack).

Validation Architecture

Test Framework

Property Value
Framework Go standard testing package
Config file None (no pytest.ini / jest.config equivalent)
Quick run command go test ./config/... ./cmd/netsynth/...
Full suite command go test ./...

Phase Requirements → Test Map

Req ID Behavior Test Type Automated Command File Exists?
RULE-01 [[rules]] block in TOML parses correctly into RawRule slice unit go test ./config/... -run TestLoadCustomRules Wave 0
RULE-01 Port field omitted → DstPort 0 (match-any) unit go test ./config/... -run TestLoadCustomRuleNoPort Wave 0
RULE-01 Missing protocol field → error unit go test ./config/... -run TestLoadCustomRuleMissingProtocol Wave 0
RULE-01 Missing class field → error unit go test ./config/... -run TestLoadCustomRuleMissingClass Wave 0
RULE-01 Typo in [[rules]] field → error naming bad key unit go test ./config/... -run TestLoadCustomRuleUnknownField Wave 0
RULE-02 User rule for port 443/tcp fires before built-in HTTPS rule unit go test ./config/... -run TestUserRulesPrepend Wave 0
RULE-03 Class name with no [sounds.*] entry gets auto-freq entry in FreqCfgs unit go test ./config/... -run TestAutoFreqAssignment Wave 0
RULE-03 Auto-freq is deterministic (same class name → same frequency) unit go test ./config/... -run TestAutoFreqDeterministic Wave 0
RULE-03 Built-in class names NOT overwritten by auto-freq unit go test ./config/... -run TestAutoFreqSkipsBuiltins Wave 0
CFG-06 --print-config flag registered on root command unit go test ./cmd/netsynth/... -run TestPrintConfigFlagRegistered Wave 0
CFG-06 --print-config exits without requiring -i flag unit go test ./cmd/netsynth/... -run TestPrintConfigNoInterface Wave 0
CFG-06 Print-config output contains all 14 default class names unit go test ./config/... -run TestPrintConfigContainsAllClasses Wave 0
CFG-06 Print-config output contains [[rules]] section when user rules are present unit go test ./config/... -run TestPrintConfigContainsRules Wave 0
CFG-06 Print-config includes source path in header comment when config loaded unit go test ./config/... -run TestPrintConfigSourcePath Wave 0

Sampling Rate

  • Per task commit: go test ./config/... ./cmd/netsynth/...
  • Per wave merge: go test ./...
  • Phase gate: go test ./... green before /gsd:verify-work

Wave 0 Gaps

All test functions listed above are new — no existing test file covers Phase 7 behavior. Tests should be added to:

  • config/config_test.go — all config package tests (follow existing writeTOML helper pattern)
  • cmd/netsynth/main_test.go — all cmd/netsynth flag tests (follow existing newTestCmd() pattern)

No new test files needed — extend the existing test files.

Open Questions

  1. PrintConfig annotation tracking for auto-assigned classes

    • What we know: addAutoFreqEntries() adds entries to FreqCfgs for new class names
    • What's unclear: PrintConfig() needs to know which entries are auto-assigned (vs default vs user-override) to annotate them correctly
    • Recommendation: Add an AutoClasses map[classify.TrafficClass]bool field to LoadResult, populated by addAutoFreqEntries(). PrintConfig() consults this map.
  2. Cobra --print-config placement: before or after config load

    • What we know: --print-config needs config loaded to show effective values
    • What's unclear: What if the user runs netsynth --print-config with no config file?
    • Recommendation: Always call config.Load(configPath) before printing. If no config file is found (auto-discovery returns nothing), the output shows all defaults — which is the most useful behavior.

Sources

Primary (HIGH confidence)

  • Source code: config/config.go — read directly; rawConfig struct, Load(), merge(), validate(), parseFile() all verified
  • Source code: classify/rules.go — DefaultRules slice, Rule struct verified
  • Source code: classify/classifier.go — NewClassifier(rules []Rule) injection point verified
  • Source code: synth/bank.go — NewBank(tau, cfgs) accepts arbitrary class maps confirmed
  • Source code: synth/config.go — ClassFreqConfigs frequency range 651047 Hz confirmed; WaveformPresetHarmonics verified
  • Source code: cmd/netsynth/main.go — current Load() call site; listIfaces early-exit pattern verified
  • Experimental: BurntSushi/toml Undecoded() behavior with [[rules]] — verified by running test code against go.mod-pinned v1.6.0
  • Experimental: FNV-32a frequency distribution — verified by running Go code; 24 distinct frequencies in 12002400 Hz range

Secondary (MEDIUM confidence)

  • Source code: config/config_test.go — test patterns for writeTOML, Load() behavior; test style confirmed
  • Source code: cmd/netsynth/main_test.go — newTestCmd() pattern, flag registration test style confirmed

Tertiary (LOW confidence)

  • None.

Metadata

Confidence breakdown:

  • Standard stack: HIGH — no new dependencies; all libraries verified against go.mod
  • Architecture: HIGH — all integration points verified by reading actual source code
  • Pitfalls: HIGH — Pitfalls 1, 2, 4, 5 verified by code inspection; Pitfall 3 experimentally verified as non-issue
  • Test patterns: HIGH — existing test file structure read directly

Research date: 2026-03-26 Valid until: Stable — 90 days (pure Go, no external dependencies, stable TOML library)