From aa1aca29a8deb880bfe14faa6b150144665ee1d2 Mon Sep 17 00:00:00 2001 From: Markus Graf Date: Fri, 27 Mar 2026 16:20:37 +0100 Subject: [PATCH] docs(11): research synthesis-and-config-layer phase --- .../11-RESEARCH.md | 412 ++++++++++++++++++ 1 file changed, 412 insertions(+) create mode 100644 .planning/phases/11-synthesis-and-config-layer/11-RESEARCH.md diff --git a/.planning/phases/11-synthesis-and-config-layer/11-RESEARCH.md b/.planning/phases/11-synthesis-and-config-layer/11-RESEARCH.md new file mode 100644 index 0000000..0849231 --- /dev/null +++ b/.planning/phases/11-synthesis-and-config-layer/11-RESEARCH.md @@ -0,0 +1,412 @@ +# Phase 11: Synthesis and Config Layer - Research + +**Researched:** 2026-03-27 +**Domain:** Go — synth config completion, PrintConfig group headers, TOML [groups] table +**Confidence:** HIGH + +## Summary + +Phase 11 completes the synthesis layer for all 35 traffic classes and adds group-aware output to `PrintConfig`. The work is entirely within `synth/config.go`, `config/config.go`, and `classify/types.go` — no new packages, no external dependencies beyond what is already in go.mod. The frequency allocation table in `synth/config.go` lines 74–110 is the authoritative source of truth for all Hz, waveform, group, and pan values for the 18 table-designed classes. LDAP, Kerberos, and Syslog get their Hz values from `autoAssignFreq` (verified by running the FNV hash: LDAP=2950 Hz, Kerberos=3250 Hz, Syslog=3050 Hz) and use Triangle waveform / Infrastructure group, matching existing Infrastructure family members. + +The test suite is currently broken in `config` and `synth` packages because `ClassFreqConfigs` only has 14 entries while `AllClasses()` returns 32. After Phase 11, `AllClasses()` returns 35 and `ClassFreqConfigs` must match exactly. Multiple existing test hardcodes (`want 14`, `TestNewBankHas14Layers`, `TestAllClassesCount want 32`) need updating to 35. + +The `[groups]` TOML feature requires adding a `Groups map[string]string` field to `rawConfig`, wiring it through `Load()`, storing the reassignments in `LoadResult`, applying them in `PrintConfig`, and adding a warning for unknown class names. No existing config pipeline stages need structural changes — group reassignment is a post-merge overlay on FreqConfig.Group fields. + +**Primary recommendation:** Execute in three sequential sub-tasks: (1) add 21 ClassFreqConfigs entries + update AllClasses() + fix count tests, (2) refactor PrintConfig for group headers, (3) add [groups] TOML support + update tests. + +--- + + +## User Constraints (from CONTEXT.md) + +### Locked Decisions +- **D-01:** Add ClassFreqConfigs entries for all 18 new classes currently in AllClasses(). Use Hz values, waveforms, pans, and groups from the Phase 9 frequency allocation table comment in `synth/config.go` lines 74-110. Use `WaveformPresetHarmonics()` for all new entries (not hand-tuned harmonics). +- **D-02:** Add ClassLDAP, ClassKerberos, ClassSyslog to AllClasses() in `classify/types.go`. Create ClassFreqConfigs entries for them using `autoAssignFreq`-derived Hz values (FNV hash in [2500, 4000] Hz range). Their group is "Infrastructure", waveform is Triangle (matching the Infrastructure family pattern). +- **D-03:** After D-01 and D-02, AllClasses() returns 35 entries (32 + 3). TestAllClassesCount updated to 35. +- **D-04:** PrintConfig groups classes by their Group field value. Each group gets a comment header line: `# ` followed by a blank line, then all classes in that group. Groups are ordered: Infrastructure, Web, Mail, Remote Access, File Transfer, Database, Discovery, VoIP, Unknown. +- **D-05:** Within each group, classes are ordered by ascending BaseHz (matching the frequency allocation table order). +- **D-06:** User-defined classes (not in AllClasses but in FreqCfgs) are emitted after all built-in groups under a "# User-defined" section header. +- **D-07:** Users define group reassignments in TOML with a `[groups]` table using simple key-value pairs: `IMAP = "Web"` reassigns IMAP from Mail to Web group. The key is the TrafficClass string value, the value is the target group name. +- **D-08:** Group reassignment only affects `--print-config` output grouping and the Group field in FreqConfig. It does NOT change frequency, waveform, or pan — those stay as designed. PrintConfig reflects the reassignment. +- **D-09:** Unknown group names in `[groups]` config are accepted (user can invent custom group names). Unknown class names produce a warning (same pattern as `[sounds.X]` with unknown class). + +### Claude's Discretion +- Exact Hz values for LDAP, Kerberos, Syslog (computed from autoAssignFreq FNV hash) +- Pan positions for LDAP, Kerberos, Syslog +- Test structure for new ClassFreqConfigs entries and PrintConfig group output +- Whether to add `[groups]` to rawConfig struct as `map[string]string` or a custom type +- How to handle group reassignment in the merge/load pipeline + +### Deferred Ideas (OUT OF SCOPE) +None — discussion stayed within phase scope. + + +--- + + +## Phase Requirements + +| ID | Description | Research Support | +|----|-------------|------------------| +| GRP-02 | `--print-config` output organizes classes by group with section headers | PrintConfig refactor: iterate canonical group order, emit `# ` header + blank line, sort within group by BaseHz ascending; user-defined classes under `# User-defined` | +| GRP-03 | Users can define `[groups]` in TOML config to reassign protocols to different sound families | Add `Groups map[string]string` to rawConfig; apply after merge in Load(); warn on unknown class name; store in LoadResult; PrintConfig reads Group field from FreqConfig | + + +--- + +## Standard Stack + +Phase 11 uses only packages already in go.mod. No new dependencies. + +### Core (already in go.mod) +| Library | Version | Purpose | Why Standard | +|---------|---------|---------|--------------| +| `github.com/BurntSushi/toml` | existing | TOML decode — `map[string]string` for `[groups]` table | Already used; `md.Undecoded()` handles unknown key detection | +| `github.com/netsynth/netsynth/classify` | local | TrafficClass constants, AllClasses() | Local package; phase modifies it | +| `github.com/netsynth/netsynth/synth` | local | FreqConfig, ClassFreqConfigs, WaveformPresetHarmonics | Local package; phase adds 21 entries | + +**No installation needed.** `go test ./...` is the only verification command. + +--- + +## Architecture Patterns + +### Pattern 1: ClassFreqConfigs Entry Structure +**What:** Each entry in `ClassFreqConfigs` follows a strict pattern — BaseHz from the frequency table, WaveformType constant, Harmonics generated by `WaveformPresetHarmonics`, Pan from the table, Group string. +**When to use:** For all 18 table-designed new entries (D-01) and 3 auto-assigned entries (D-02). +**Example (from existing code):** +```go +classify.ClassIMAP: { + BaseHz: 241.0, + WaveformType: WaveformTriangle, + Harmonics: WaveformPresetHarmonics(WaveformTriangle, 241.0, SampleRate), + Pan: 0.3, + Group: "Mail", +}, +``` + +### Pattern 2: autoAssignFreq for LDAP/Kerberos/Syslog +**What:** FNV-32a hash into [2500, 4000] Hz range. Values are deterministic — computed once and hardcoded in the entry. +**Verified computed values:** +- LDAP: 2950.0 Hz +- Kerberos: 3250.0 Hz +- Syslog: 3050.0 Hz + +Pan positions (Claude's discretion): assign spread within [-0.3, 0.3] range — LDAP=-0.2, Kerberos=0.0, Syslog=0.2 keeps them center-ish (Infrastructure family is already spread -0.3 to +0.3 at low frequencies; these are high-frequency so center-cluster is appropriate). + +### Pattern 3: AllClasses() Update (classify/types.go) +**What:** Add ClassLDAP, ClassKerberos, ClassSyslog inside the Infrastructure block in AllClasses(). The comment guard `// D-01: no ClassFreqConfigs until Phase 11` is removed. +**Ordering within Infrastructure:** Append after ClassSNMP (highest-Hz member at 118 Hz) since LDAP/Kerberos/Syslog are at 2950/3250/3050 Hz — technically they sort higher but within their family group they follow table order. + +### Pattern 4: PrintConfig Group-Ordered Iteration (GRP-02) +**What:** Replace the current flat `classify.AllClasses()` iteration with a canonical-group-ordered iteration. Build a `groupOrder []string` slice with the 9 canonical groups; for each group collect classes from AllClasses() whose `FreqCfgs[cls].Group == group`, sort by BaseHz, emit group header + entries. + +**Canonical group order (D-04):** +``` +Infrastructure, Web, Mail, Remote Access, File Transfer, Database, Discovery, VoIP, Unknown +``` + +**Implementation approach:** +```go +// groupOrder defines canonical display order for --print-config +var groupOrder = []string{ + "Infrastructure", "Web", "Mail", "Remote Access", + "File Transfer", "Database", "Discovery", "VoIP", "Unknown", +} + +// Build group -> []TrafficClass index from AllClasses() +// Sort each group slice by FreqCfgs[cls].BaseHz ascending (D-05) +// For each group: emit "# \n\n" then entries +// After all built-in groups: user-defined classes under "# User-defined\n\n" +``` + +**Critical detail:** The sort must use `result.FreqCfgs[cls].BaseHz` (the effective config, post-merge) not `synth.ClassFreqConfigs[cls].BaseHz`, so user frequency overrides are reflected in the sort order. This is unlikely to matter in practice but is correct. + +### Pattern 5: TOML [groups] Support (GRP-03) + +**rawConfig struct extension:** +```go +type rawConfig struct { + Sounds map[string]SoundOverride `toml:"sounds"` + Rules []RawRule `toml:"rules"` + Groups map[string]string `toml:"groups"` // key=TrafficClass string, val=group name +} +``` + +Using `map[string]string` (Claude's discretion) is the simplest approach — TOML decodes `[groups]` as a string map naturally. No custom type needed. + +**LoadResult extension:** +```go +type LoadResult struct { + FreqCfgs map[classify.TrafficClass]synth.FreqConfig + UserRules []classify.Rule + ConfigPath string + AutoClasses map[classify.TrafficClass]bool + GroupOverrides map[classify.TrafficClass]string // NEW: class -> reassigned group name +} +``` + +**applyGroupOverrides function (new):** After `merge()` in `Load()`, iterate `raw.Groups`; for each key=className, val=groupName: if className is known (exists in freqCfgs), update `cfg.Group = groupName` and store back; if unknown, emit warning to stderr (D-09). The warning uses the same pattern as unknown [sounds.X] classes. + +**Alternative approach:** Apply group reassignment directly inside PrintConfig by reading `result.GroupOverrides` map on-the-fly without mutating FreqConfig. This avoids touching LoadResult but makes PrintConfig depend on a new field anyway. Mutating FreqConfig.Group is cleaner because classAnnotation and other consumers see a consistent view. + +### Pattern 6: TOML md.Undecoded() and the [groups] map +**Critical:** `BurntSushi/toml` decodes map fields without flagging individual map keys as "undecoded" — unknown class names in `[groups]` will NOT be caught by `md.Undecoded()`. This is the same behavior as `[sounds.*]` (unknown class names are silently accepted at decode time and validated in `merge()`). The warning for unknown class names in `[groups]` must be implemented in the new `applyGroupOverrides` function, not in `parseFile`. This matches D-09 exactly. + +### Anti-Patterns to Avoid +- **Hand-computing harmonics for new entries:** Use `WaveformPresetHarmonics()` — don't write `[]HarmonicDef{{1, 1.0}, {2, 0.5}, ...}` manually for table-designed entries. +- **Sorting AllClasses() output by BaseHz globally:** The table order in AllClasses() is the authoritative display order within families. The sort in PrintConfig must use group-then-BaseHz, not a flat sort. +- **Adding [groups] validation to parseFile:** `md.Undecoded()` cannot catch unknown string map keys; validation must be in `applyGroupOverrides`. +- **Mutating synth.ClassFreqConfigs:** copyDefaults() produces a working copy; all mutations go there, never to the package-level map. + +--- + +## Don't Hand-Roll + +| Problem | Don't Build | Use Instead | Why | +|---------|-------------|-------------|-----| +| Harmonic series for new waveform entries | Manual `[]HarmonicDef` slices | `WaveformPresetHarmonics(wt, baseHz, SampleRate)` | Already computes bandlimited series; manual values would drift if SampleRate changes | +| FNV hash for LDAP/Kerberos/Syslog Hz | Custom hash | `autoAssignFreq("LDAP")` output hardcoded as 2950.0 | Values are pre-computed; hardcoding eliminates runtime dependency on config package from synth package | +| TOML group reassignment validation | Custom struct with Validate() | `map[string]string` + warning in applyGroupOverrides | Matches existing pattern for [sounds.*] unknown classes | + +--- + +## Runtime State Inventory + +Step 2.5: SKIPPED — this is not a rename/refactor/migration phase. + +--- + +## Environment Availability Audit + +Step 2.6: SKIPPED — phase is purely code changes within existing Go packages. No external tools, services, databases, or CLI utilities beyond the existing `go` toolchain are required. The Go toolchain is already confirmed working (tests ran above). + +--- + +## Common Pitfalls + +### Pitfall 1: Stale hardcoded counts in tests +**What goes wrong:** `TestNewBankHas14Layers` expects 14 layers; `TestLoadNoConfig`/`TestLoadUnknownClass`/`TestLoadAllDefaultsPresent`/`TestLoadNoConfigReturnsLoadResult` all assert `len(cfgs) == 14`; `TestAllClassesCount` asserts 32. After adding 21 ClassFreqConfigs entries and 3 AllClasses entries these fail. +**Why it happens:** Tests were written against the Phase 9 baseline of 14 built-in entries and never updated. +**How to avoid:** Update all hardcoded count assertions to 35 in a single pass after D-01 and D-02 are complete. The specific files and line numbers: +- `synth/bank_test.go:12` — `want 14` → `want 35` +- `synth/bank_test.go:13` — `len(b.layers) != 14` → `!= 35` +- `config/config_test.go:120` — `want 14` → `want 35` +- `config/config_test.go:149` — `want 14 (BOGUS should not appear)` → `want 35` +- `config/config_test.go:185` — `want 14` → `want 35` +- `config/config_test.go:617` — `want 14` → `want 35` +- `classify/classifier_test.go:459` — `want 32` → `want 35` +**Warning signs:** Any test that hardcodes a count of 14 or 32. + +### Pitfall 2: TestPrintConfigContainsAllClasses class list is stale +**What goes wrong:** `config/config_test.go:407–416` lists exactly 14 class name strings. After Phase 11, AllClasses() has 35 entries; the test should either check all 35 or be replaced by a loop over `classify.AllClasses()`. +**How to avoid:** Replace the hardcoded `classNames` slice with `for _, cls := range classify.AllClasses()` during PrintConfig test updates. + +### Pitfall 3: PrintConfig group-sort uses default Hz, not effective Hz +**What goes wrong:** If group-sort code reads `synth.ClassFreqConfigs[cls].BaseHz` instead of `result.FreqCfgs[cls].BaseHz`, user frequency overrides don't affect sort position. The output is still correct for default configs but fails when the user overrides a frequency in [sounds.*]. +**How to avoid:** Always sort from `result.FreqCfgs[cls].BaseHz`. + +### Pitfall 4: [groups] map keys not caught by md.Undecoded() +**What goes wrong:** `parseFile` uses `md.Undecoded()` to catch field typos. TOML map keys are all valid decode targets by definition — `md.Undecoded()` will be empty even if `[groups]` contains `IMAPtypo = "Web"`. The warning must come from `applyGroupOverrides`. +**How to avoid:** Implement unknown-class warning in `applyGroupOverrides`, not in `parseFile` or `validate`. + +### Pitfall 5: TestClassFreqConfigsMatchAllClasses fails if counts diverge +**What goes wrong:** `synth/config_test.go:57` checks `len(synth.ClassFreqConfigs) == len(classify.AllClasses())`. If the implementer adds all 21 ClassFreqConfigs entries but only adds 2 of the 3 new AllClasses entries, this test fails in a confusing way. +**How to avoid:** D-01 and D-02 must be completed atomically — add all 21 ClassFreqConfigs entries and all 3 AllClasses entries in a single task. + +### Pitfall 6: "File Transfer" group name has a space +**What goes wrong:** If the group sort map uses `"FileTransfer"` instead of `"File Transfer"` (matching the FreqConfig.Group string), Discovery and File Transfer classes end up under a catch-all or dropped entirely. +**How to avoid:** All group strings must exactly match those in `FreqConfig.Group`. Canonical list: `"Infrastructure"`, `"Web"`, `"Mail"`, `"Remote Access"`, `"File Transfer"`, `"Database"`, `"Discovery"`, `"VoIP"`, `"Unknown"`. + +--- + +## Code Examples + +### Complete frequency table for all 21 missing entries +From the allocation table in `synth/config.go` lines 74–110 plus D-02 computed values: + +``` +// Infrastructure additions (Triangle, [93-118] Hz) +mDNS: 93 Hz, Triangle, pan=+0.3 +SSDP: 105 Hz, Triangle, pan=-0.2 +SNMP: 118 Hz, Triangle, pan=+0.2 +// Web addition (Sawtooth) +QUIC: 190 Hz, Sawtooth, pan=-0.2 +// Mail additions (Triangle) +IMAP: 241 Hz, Triangle, pan=+0.3 +POP3: 271 Hz, Triangle, pan=+0.4 +SMTP-sub: 305 Hz, Triangle, pan=+0.5 +// Remote Access additions (Square) +RDP: 385 Hz, Square, pan=-0.6 +Telnet: 432 Hz, Square, pan=-0.5 +VNC: 485 Hz, Square, pan=-0.4 +// File Transfer additions (Square) +FTP: 545 Hz, Square, pan=+0.5 +SMB: 612 Hz, Square, pan=+0.6 +TFTP: 687 Hz, Square, pan=+0.7 +// Database additions (Sawtooth) +MySQL: 1543 Hz, Sawtooth, pan=-0.4 +PostgreSQL: 1732 Hz, Sawtooth, pan=-0.2 +Redis: 1944 Hz, Sawtooth, pan=+0.2 +MongoDB: 2182 Hz, Sawtooth, pan=+0.4 +// VoIP (Sine) +SIP: 2449 Hz, Sine, pan=0.0 +// Infrastructure auto-assigned (Triangle) — D-02 +LDAP: 2950 Hz, Triangle, pan=-0.2 +Kerberos: 3250 Hz, Triangle, pan=0.0 +Syslog: 3050 Hz, Triangle, pan=+0.2 +``` + +### PrintConfig group header pattern (D-04) +```go +// groupOrder is the canonical display order for --print-config section headers. +var groupOrder = []string{ + "Infrastructure", "Web", "Mail", "Remote Access", + "File Transfer", "Database", "Discovery", "VoIP", "Unknown", +} + +// In PrintConfig, replace the flat AllClasses() loop with: +builtinByGroup := map[string][]classify.TrafficClass{} +for _, cls := range classify.AllClasses() { + cfg := result.FreqCfgs[cls] + grp := cfg.Group + builtinByGroup[grp] = append(builtinByGroup[grp], cls) +} +// Sort each group slice by BaseHz ascending (D-05) +for grp := range builtinByGroup { + sort.Slice(builtinByGroup[grp], func(i, j int) bool { + return result.FreqCfgs[builtinByGroup[grp][i]].BaseHz < + result.FreqCfgs[builtinByGroup[grp][j]].BaseHz + }) +} +// Emit in canonical order +for _, grp := range groupOrder { + classes, ok := builtinByGroup[grp] + if !ok || len(classes) == 0 { + continue + } + fmt.Fprintf(&sb, "# %s\n\n", grp) + for _, cls := range classes { + // ... existing per-class emit logic ... + } +} +``` + +### [groups] TOML apply pattern (D-07/D-08/D-09) +```go +// applyGroupOverrides overlays [groups] reassignments onto freqCfgs.Group in-place. +// Unknown class names produce a warning; unknown group names are silently accepted (D-09). +func applyGroupOverrides(cfgs map[classify.TrafficClass]synth.FreqConfig, groups map[string]string) { + for className, groupName := range groups { + cls := classify.TrafficClass(className) + cfg, known := cfgs[cls] + if !known { + fmt.Fprintf(os.Stderr, "Warning: config: [groups]: unknown class %q (ignored)\n", className) + continue + } + cfg.Group = groupName + cfgs[cls] = cfg + } +} +``` + +Call site in `Load()` — after `merge()`: +```go +addAutoFreqEntries(freqCfgs, userRules, autoClasses) +merge(freqCfgs, raw.Sounds) +applyGroupOverrides(freqCfgs, raw.Groups) // NEW +``` + +--- + +## Validation Architecture + +nyquist_validation is enabled (not false in config.json). + +### Test Framework +| Property | Value | +|----------|-------| +| Framework | Go testing (stdlib) | +| Config file | none — `go test ./...` | +| Quick run command | `go test ./classify/... ./synth/... ./config/...` | +| Full suite command | `go test ./...` | + +### Current Test Failures (baseline — must fix) +The following tests are currently failing and Phase 11 must make them green: + +| Test | Package | Failure Cause | Fix Required | +|------|---------|---------------|--------------| +| `TestAllClassesHaveConfig` | `synth` | 18 new classes in AllClasses() missing from ClassFreqConfigs | Add 18+3 entries | +| `TestClassFreqConfigsMatchAllClasses` | `synth` | 14 != 32 | Add entries, update AllClasses | +| `TestNewBankHas14Layers` | `synth` | 14 != 32 + missing layers | Add entries + update assert | +| `TestLoadAllDefaultsPresent` | `config` | 18 classes missing from loaded map | Add ClassFreqConfigs entries | +| `TestLoadNoConfig` | `config` | len==14 assert, now 35 | Update count | +| `TestLoadUnknownClass` | `config` | len==14 assert | Update count | +| `TestLoadNoConfigReturnsLoadResult` | `config` | len==14 assert | Update count | +| `TestPrintConfigContainsAllClasses` | `config` | Hardcoded 14 class names | Expand to all 35 | +| `TestAllClassesCount` | `classify` | want 32, still passes (no new classes yet) | Update to 35 after D-02 | + +### Phase Requirements to Test Map +| Req ID | Behavior | Test Type | Automated Command | Test Exists? | +|--------|----------|-----------|-------------------|-------------| +| GRP-02 | PrintConfig shows group headers | unit | `go test ./config/... -run TestPrintConfig` | Partial — `TestPrintConfigContainsAllClasses` exists; new `TestPrintConfigGroupHeaders` needed | +| GRP-02 | Each built-in class appears in correct group | unit | `go test ./config/... -run TestPrintConfigGroupHeaders` | No — Wave 0 | +| GRP-03 | [groups] TOML reassigns class to different group | unit | `go test ./config/... -run TestLoadGroupOverride` | No — Wave 0 | +| GRP-03 | Unknown class in [groups] produces warning | unit | `go test ./config/... -run TestLoadGroupUnknownClass` | No — Wave 0 | +| GRP-03 | PrintConfig reflects group reassignment | unit | `go test ./config/... -run TestPrintConfigGroupReassignment` | No — Wave 0 | + +### Sampling Rate +- **Per task commit:** `go test ./classify/... ./synth/... ./config/...` +- **Per wave merge:** `go test ./...` +- **Phase gate:** `go test ./...` fully green before `/gsd:verify-work` + +### Wave 0 Gaps +- [ ] `config/config_test.go` — add `TestPrintConfigGroupHeaders` (asserts `# Infrastructure`, `# Mail`, etc. appear in output in correct order) +- [ ] `config/config_test.go` — add `TestLoadGroupOverride` (TOML `[groups]\nIMAP = "Web"` → FreqCfgs[IMAP].Group == "Web") +- [ ] `config/config_test.go` — add `TestLoadGroupUnknownClass` (TOML `[groups]\nBOGUS = "Web"` → no error, warning to stderr) +- [ ] `config/config_test.go` — add `TestPrintConfigGroupReassignment` (LoadResult with group-reassigned IMAP shows IMAP under `# Web` not `# Mail`) +- [ ] `config/config_test.go` — update `TestPrintConfigContainsAllClasses` classNames slice to cover all 35 (or replace with AllClasses() loop) + +--- + +## Open Questions + +1. **Pan positions for LDAP, Kerberos, Syslog (Claude's discretion)** + - What we know: Infrastructure family uses [-0.3, +0.3] pan range for existing members (ICMP=-0.3, NTP=-0.1, DHCP=+0.1, DNS=0.0, mDNS=+0.3, SSDP=-0.2, SNMP=+0.2) + - Recommendation: LDAP=-0.2, Kerberos=0.0, Syslog=+0.2 — fills the same spread pattern without exact collision with existing values + +2. **TestNewBankHas14Layers function name** + - What we know: The CONTEXT.md calls out that the name is stale (should reflect 35 classes) + - Recommendation: Rename to `TestNewBankHasAllLayers` and update the assertion to `len(b.layers) != len(classify.AllClasses())` — this future-proofs it against further class additions + +3. **Discovery group — no existing built-in entries in AllClasses() have Group="Discovery"** + - What we know: mDNS, SSDP, SNMP are currently in AllClasses() but not in ClassFreqConfigs (they are 3 of the 18 missing entries); they belong to "Infrastructure" per the frequency allocation table (slots 3-5, Group=Infrastructure) + - Clarification needed: the frequency table marks them as Infrastructure, but the canonical group order in D-04 lists "Discovery" as a separate group. The table must take precedence — mDNS/SSDP/SNMP are Group="Infrastructure", not "Discovery". The "Discovery" slot in groupOrder may remain empty or be omitted from PrintConfig output (the `if !ok || len(classes) == 0 { continue }` guard handles this). + +--- + +## Sources + +### Primary (HIGH confidence) +- Direct code inspection of `/home/dev/workspace/yoloyolo/synth/config.go` — frequency allocation table lines 74-110 (authoritative Hz, waveform, pan, group values) +- Direct code inspection of `/home/dev/workspace/yoloyolo/config/config.go` — PrintConfig, rawConfig struct, merge(), autoAssignFreq(), addAutoFreqEntries() +- Direct code inspection of `/home/dev/workspace/yoloyolo/classify/types.go` — AllClasses(), TrafficClass constants +- Computed autoAssignFreq values by running the FNV hash Go code — LDAP=2950, Kerberos=3250, Syslog=3050 +- `go test ./...` output — confirmed exact failing tests and error messages + +### Secondary (MEDIUM confidence) +- CONTEXT.md decisions D-01 through D-09 — user decisions locked in prior /gsd:discuss-phase session +- STATE.md accumulated decisions — confirms Phase 10 left AllClasses() at 32 intentionally pending Phase 11 + +--- + +## Metadata + +**Confidence breakdown:** +- Standard stack: HIGH — no new dependencies; entire phase is code within existing packages +- Architecture: HIGH — patterns derived directly from existing code and locked CONTEXT.md decisions +- Pitfalls: HIGH — derived from actual failing test output and code inspection +- autoAssignFreq Hz values: HIGH — computed by executing the actual FNV hash code + +**Research date:** 2026-03-27 +**Valid until:** 2026-04-27 (stable — no external packages change)