diff --git a/diylc/docs/plans/audio-coverage-roadmap.md b/diylc/docs/plans/audio-coverage-roadmap.md new file mode 100644 index 00000000..391908af --- /dev/null +++ b/diylc/docs/plans/audio-coverage-roadmap.md @@ -0,0 +1,619 @@ +# Audio Coverage Roadmap + +Closing the gap between the component library and what the guitar, amplifier, pedal and hi-fi +communities actually build. + +Status: proposed +Scope: `diylc-library` (`components.passive`, `.tube`, `.electromechanical`, `.guitar`, +`.connectivity`), one bug fix in `diylc-core/serialization` +Companion: the sibling roadmaps in this directory, which apply the same method to other packages + +## 1. Goal + +The audio side of the library has never had a systematic coverage pass, and it is the side the +sample corpus is made of. The headline number: **three components in the `Tubes` category**, +against 50 corpus projects that place a tube socket. + +This document inventories what ships, measures what people hand-draw because it does not exist, +and sorts the additions into three tiers by evidence of demand rather than by appetite. + +## 2. Decisions that govern this roadmap + +| # | Decision | Choice | Rationale | +|---|---|---|---| +| D1 | What counts as a gap | **A part people are drawing by hand today.** Measured from label text in `diylc-regression-data/input`, not from catalogue browsing. | Keeps the roadmap honest. Several things that looked like gaps from the palette listing (pedal enclosures, EMG pickups, Filtertrons, Tele single coils, the switch-type matrix) turned out to ship already; several things that look exotic turn out to be in 25 projects. | +| D2 | Evidence vs. judgement | **Tiers 1 and 2 are measured. Tier 3 is reasoned and labelled as such.** | The corpus is overwhelmingly pedal and amp. It cannot speak to hi-fi demand at all, and saying so is better than inventing a number. See §4.3. | +| D3 | New class vs. new enum constant | **Same footprint family → enum constant; different structure → new class.** | Already the established paradigm, and `diylc/CLAUDE.md` states the preference directly: a `Version` property on one class beats a class per revision. | +| D4 | Where iron lives | **One `Transformer` class with a `Type` property (power / output / choke), in `components.passive`.** Not three classes. | A bell-end power transformer, an output transformer and a choke share an outline, a mounting footprint and a tap-strip; they differ in tap count and label. `AudioTransformer` already sits in `passive` and already extends `AbstractMultiPartComponent`, so the neighbour is right there. | +| D5 | Backward compatibility | Append-only enums, unchanged defaults, unchanged control-point counts for existing variants. | `.diy` files are a public contract. Only §6.1 touches serialization, and it *restores* compatibility rather than risking it. | +| D6 | Pre-set data before components | **Where a variant or a datasheet row closes a gap, it ships before any new class.** | Both are configuration rather than Java, and both are already covered by tests. §4.2 shows which of the most-placed parts have neither. | +| D7 | Composable parts are not components | **If a part can be assembled from existing components and saved as a building block, it ships as a factory building block, not a new class.** | A new class has to be drawn, transformed, tested, BOM-policed and supported forever, and it crowds the toolbox. A building block is data. The test is whether the assembly is electrically and visually honest without a new primitive: a push-pull pot is a pot plus a DPDT switch and passes, a chassis transformer is not a rectangle plus lugs and fails. See §5.4. | + +### Explicitly out of scope + +- The dormant classes outside the packages in scope above. They were parked deliberately + ("comment out future stuff", `27eb2599`) and belong to the roadmaps for their own packages. +- Any electrical simulation: load-line *plotting* is in scope only because the component already + exists (§6.2), impedance and frequency-response maths are not. +- Speaker cabinet and enclosure mechanical drawing beyond what `ChassisPanel` already does. +- Bumping the project version, packaging, and the deployment build. + +## 3. Current coverage + +**169 components register at runtime.** 208 classes carry a `@ComponentDescriptor`, but 39 have it +commented out and are invisible to the annotation scan. + +| Category | Active | | Category | Active | +|---|---|---|---|---| +| Schematic Symbols | 30 | | Semiconductors | 12 | +| Electro-Mechanical | 28 | | Guitar | 12 | +| Passive | 17 | | Boards | 12 | +| Connectivity | 16 | | Misc | 10 | +| Displays & Outputs | 13 | | Controllers | 10 | +| Shapes | 4 | | **Tubes** | **3** | +| SMD | 2 | | | | + +What is genuinely strong and should not be touched: the switch matrix (`ToggleSwitchType` runs SPST +through 5PDT with both on/on/on variants; sealed and open rotaries; Lever, LP, Freeway 3x3, +Schaller Megaswitch in six types, S1), the pickup models (`SingleCoilPickup` does Strat and Tele +bodies with rod/rail/dual-rail poles, `HumbuckerPickup` does PAF/Mini/Filtertron with an EMG-style +variant), and resistor coverage (22 variants from 1/4 W carbon comp to 100 W ceramic). + +### 3.1 Dormant classes relevant to audio + +Five of the 39 fall inside this roadmap's scope. Two are finished code behind a commented +annotation; three are not. + +| Class | State | To revive | +|---|---|---| +| `passive/AirCoreInductor` | Body live, 4 commented descriptor lines | Uncomment + restore 4 imports | +| `passive/BoxTrimmer` | Body live, 9 commented lines, `BoxTrimmerTransformer` exists | Uncomment + restore imports | +| `misc/Loadline` | Body live, 4 commented lines, extends `AbstractBoard` | Uncomment + restore imports | +| `passive/AxialAirCoreInductor` | **Stub** — 69 of 70 lines commented, and it extended `AbstractFilmCapacitor` | Rewrite, or delete the file | +| `smd/SOT23Package` | **Stub** — 427 of 449 lines commented | Out of scope here | + +## 4. Evidence + +### 4.1 Method + +`diylc-regression-data/input` holds 709 `.diy` projects (cloud 379, v1 284, user-files 34, +diy-fever 12). The 284 v1-format files carry no class names, so component tallies reflect the 425 +modern files; label-text searches cover all 709. + +**Searching raw file text does not work.** `Image` components store base64, and three-letter +queries hit it: `XLR` returned 17 "files" of which zero were real. Every figure below comes from +extracting ``, ``, `<name>`, `<value>` and `<description>` element contents first +(118,619 strings) and searching those. + +### 4.2 Two coverage mechanisms, and which parts have neither + +Pre-set data reaches a component two ways, and they are easy to confuse. + +**Variants** (`variants.xml`, 176 of them) are saved property sets, authored per class, listed under +*Apply Variant*. + +**Datasheets** are parametrized part catalogues: a `<ClassName>.datasheet` CSV on the classpath, one +row per real orderable part, listed under *Apply Model*. Five capacitor classes have one, and +between them they carry **3,539 rows covering 34 models** — each row a part number, a voltage, a +capacitance, exact body dimensions in mm, and body/border/label/marker colours: + +| Class | Rows | Models | +|---|---|---| +| `RadialFilmCapacitor` | 1,523 | 14 (WIMA MKS-4/MKP-4/FKP/FKS, Orange Drop 225P/715P/716P, Epcos MKT) | +| `RadialElectrolytic` | 833 | 7 (Panasonic FC/FM/NHG, Nichicon FW/FG/KZ, Elna Silmic II) | +| `AxialElectrolyticCapacitor` | 455 | 4 (F&T Typ A, Sprague TVA Atom, Illinois TTA, JJ ANH) | +| `RadialMicaCapacitor` | 433 | 7 (Cornell Dubilier CD10 through CD42, CDV19/CDV30) | +| `AxialFilmCapacitor` | 293 | 2 (Solen Fast, Mallory 150) | + +**Capacitors are therefore already covered, at a fidelity no variant could reach** — exact +dimensions per value and voltage, not a size estimate. Any roadmap item proposing to "add +Mallory 150 / Orange Drop / WIMA variants" is proposing something that shipped long ago; an +earlier draft of this document made exactly that mistake by counting only `variants.xml`. + +The gap is the classes with *neither* mechanism: + +| Component | Placements | Variants | Datasheet | +|---|---|---|---| +| `Resistor` | 2,388 | 22 | — | +| `RadialFilmCapacitor` | 754 | 0 | 1,523 rows | +| `RadialElectrolytic` | 602 | 0 | 833 rows | +| `TransistorTO92` | 363 | 0 | **none** | +| `DiodePlastic` | 324 | 0 | **none** | +| `RadialCeramicDiskCapacitor` | 283 | 0 | **none** | +| `PotentiometerPanel` | 195 | 2 | **none** | + +`RadialCeramicDiskCapacitor` is the odd one out among the capacitors — the only one of the six with +no datasheet and no variants, despite 283 placements. `TantalumCapacitor` and `MultiSectionCapacitor` +are likewise uncovered. + +The opportunity is that **the datasheet mechanism is generic and nothing outside capacitors uses +it.** `IDatasheetSupport` is a one-method interface in `diylc-core` implemented by exactly those five +classes; see §5.3 for what extending it costs. + +### 4.3 Hand-written labels for parts that do not exist + +Out of 709 projects: + +| Part | Files | Mentions | Status in library | +|---|---|---|---| +| Output transformer (`OT`, "output transformer") | 27 / 10 | 84 / 10 | **absent** | +| Power transformer (`PT`, "power transformer") | 15 / 12 | 37 / 25 | **absent** | +| Speaker | 25 | 45 | **absent**, not even a symbol | +| Choke | 21 | 38 | **absent** | +| 3PDT / stomp / footswitch | 12 / 6 / 5 | 12 / 8 / 8 | electrically yes, body no | +| Push-pull pot | 12 | 15 | **absent** | +| Standby | 10 | 31 | covered by toggle | +| LDR | 10 | 15 | **absent** as a discrete part | +| Reverb tank | 6 | — | **absent** | +| 1590-series enclosure | 5 | 5 | covered — `ChassisPanel` variants | + +Corroborating signals: 15 files assemble a transformer by hand from `TransformerCoil` pieces, and +88 fall back to `shapes.Rectangle`. + +**Zero hits**, and therefore Tier 3 at best: `xlr`, `binding post`, `piezo`, `jazzmaster`, +`lipstick`, `varitone`, `concentric`, `vactrol`. The corpus is pedals and amps; its silence on +hi-fi connectors is a property of the sample, not evidence of no demand. + +### 4.4 Why `EyeletBoard` is used 8 times and discrete turrets 40 + +`EyeletBoard.draw()` paints a full grid of eyelets with `fillOval`. They are decorative: no control +points, no node names, no conductive areas, nothing in the netlist. So builders ignore it and place +individual `Turret` components, which do carry a node name, on a `BlankBoard`. + +This kills the obvious-looking fix. A `Type = Turret` flag on `EyeletBoard` would inherit the same +flaw, and a full grid is wrong for turret boards anyway — they are drilled per design. The real fix +is selectable, connected positions on both boards, which is a `composite-building-blocks.md`-scale +design question. Tier 3, §8.6. + +## 5. Shared mechanics + +### 5.1 Component discovery + +Classpath annotation scan in `ComponentProcessor.getComponentTypes()`. **Any class carrying +`@ComponentDescriptor` is picked up automatically** — no manifest, no registry. Uncommenting an +annotation is the entire registration step. + +### 5.2 Serialization safety + +Per `diylc/CLAUDE.md`: never rename or delete a serialized field or change its type; fields added +to an existing class deserialize as `null` in old files, so default in the getter, not the +constructor; `serialVersionUID` stays `1L`. + +Worth knowing before touching `ProjectFileManager`: its anonymous `MapperWrapper` overrides +`shouldSerializeMember` and drops elements that map to no field, so **unknown fields in old files +are already tolerated**. It does *not* override `realClass`, so an **unknown class throws**. That +asymmetry is the whole of §6.1. + +### 5.3 Variant and datasheet mechanics + +**Variants** live in `diylc-core/src/main/resources/import-defaults/variants.xml`, an +XStream-serialized `Map<String, List<Template>>` keyed by fully-qualified class name. +`FactoryVariantsTest` (`diylc-library/src/test/java/org/diylc/components/`) loads it, resolves every +key, and asserts every template names a live property and applies cleanly — so a stale variant fails +the build rather than failing silently at runtime. + +**Datasheets** are cheaper than they look. `DatasheetService.loadDatasheet(Class)` resolves +`/datasheets/<SimpleClassName>.datasheet` off the classpath and splits each line on `\s*,\s*`. +Adding one to a component is therefore two steps and no registration: + +1. implement `IDatasheetSupport.applyModel(String[] model)` on the component, mapping columns onto + setters — `RadialFilmCapacitor.applyModel` is 20 lines and the model to copy; +2. drop `<ClassName>.datasheet` into `diylc-library/src/main/resources/datasheets/`. + +`ComponentPopupMenu.updateApplyModelMenu` then builds the *Apply Model* submenu from +`ComponentType.getDatasheet()` automatically, and `Presenter.applyModelToSelection` routes the +chosen row back to `applyModel`. + +Two cautions, both learned from §6.5: + +- **The column format is positional and unforgiving.** `CapacitorDatasheetService` reads + `model[1].split(" ")[0..1]` for voltage and `model[2].split(" ")[0..1]` for value, so a cell + must be `"<number> <unit>"` with exactly one space. `4.7uF` throws + `ArrayIndexOutOfBoundsException`; there is no validation anywhere. +- **`DatasheetService` reads with `new InputStreamReader(in)` and no charset**, so files are decoded + with the platform default. Keep datasheet files ASCII — write `uF`, never `µF`. Per JEP 400 the + default is UTF-8 on the supported JVM, so a latin-1 byte becomes U+FFFD rather than failing loudly. + +There is also an unused path worth knowing about: `DatasheetService.lookup` / +`CapacitorDatasheetService.lookup` match a datasheet row by approximate value within a tolerance, +for auto-sizing a component from its value. **It has no production caller** — only +`CapacitorDimensionServiceTests` — so it is latent infrastructure, not live behaviour. + +### 5.4 Factory building block mechanics + +Building blocks are named groups of components, saved from a selection and re-placed as a unit. +`BuildingBlockManager.importDefaultBlocks()` ships a default set in +`diylc-core/src/main/resources/import-defaults/blocks.xml`, stored as a +`Map<String, List<IDIYComponent<?>>>` using the `diylc` package alias +(`aliasPackage("diylc", "org.diylc.components")`), and merges it into the user's own blocks by name. + +Three ship today: `TO-92 w/ 3 Leads`, `TO-92 w/ 2 Leads`, `Title Block`. + +Authoring one is a UI job, not a code job: place the components, select them, *Save as Building +Block*, then lift the entry out of the user config into `blocks.xml`. Per D7 this is the default +route for anything composable. + +**The blocker (fixed in §6.6).** `importDefaultBlocks()` is wrapped in a one-shot flag: + +```java +if (!configManager.readBoolean(IPlugInPort.DEFAULT_BLOCKS_IMPORTED_KEY, false)) { +``` + +Once `defaultBlocksImported` is true it never runs again, so **a factory block added to `blocks.xml` +reaches new installs only** — never anyone who has already launched the app. Any plan that +distributes composites this way needs §6.6 first. + +`VariantManager.importDefaultVariants()` does not have this problem, and the asymmetry is worth +knowing: it keeps a `defaultTemplatesImportDate` watermark, re-reads `variants.xml` on every start, +and imports any variant whose `Template.createdOn` is newer than the watermark and whose name the +user does not already have. New factory *variants* do reach existing users today. + +### 5.5 Tests + +JUnit 4 under `diylc-library/src/test/java/org/diylc/`, with `components/passive/`, +`components/misc/` and `netlist/` already populated. Switching and netlist behaviour is unit-tested +(`AbstractSwitchTests` and its fifteen subclasses); component rendering is not — that is the +regression suite's job. Any new class with switching belongs in `netlist/`. + +### 5.6 Build and verify + +```bash +cd diylc +mvn -pl diylc-library -am test +``` + +Baseline at the time of writing, on `controllers` at `2a08fbe5`: `diylc-core` 148 tests, +`diylc-library` 331 tests, BUILD SUCCESS, project version 6.6.0. + +Everything in Tiers 1 and 2 affects rendering or serialization, so each item also wants a +regression run (`.claude/skills/regression-test/SKILL.md`) before it is called done. §6.1 +specifically cannot be verified any other way. + +### 5.7 Changelog + +User-visible additions get a `<Change>` entry in `diylc-core/src/main/resources/update.xml`, in the +block for the unreleased version. `NEW_FEATURE` for components and variants, `BUG_FIX` for §6.1. + +--- + +## 6. Tier 1 — no new components + +Enum constants, catalogue data, building blocks and uncommenting. Cheapest fidelity per hour in +the roadmap, and where D6 and D7 send most of the work. + +§6.1, §6.5 and §6.6 are correctness fixes and should go first regardless of the rest; §6.6 also +gates §6.7, which is the channel the composable parts travel through. + +### 6.1 Restore the `ElectrolyticCanCapacitor` alias — **bug, not a gap** + +`diylc-regression-data/input/cloud/diy/282.diy` contains three +`<org.diylc.components.passive.ElectrolyticCanCapacitor>` elements. That class exists nowhere in +the tree, in any module, or in the vendored jars, and `ProjectFileManager.configure()` registers no +alias for it. Per §5.2 the file cannot deserialize. It came from the cloud corpus, so real user +files are affected. + +This is the exact failure `diylc/CLAUDE.md` forbids: a shipped component class was renamed or +removed without an XStream alias. + +The old element carries `name`, `alpha`, `value` (four nulls), `orientation`, `controlPoints` +(three), `bodyColor`, `baseColor`, `borderColor`, `labelColor`, `display`, `sections` (`_1`) and +`diameter` (1.0 in). `MultiSectionCapacitor` is the successor and matches all of it except +`sections`, which it does not have — it derives section count from `value.length`. + +1. In `ProjectFileManager.configure()`, beside the existing aliases: + ```java + xStream.alias("org.diylc.components.passive.ElectrolyticCanCapacitor", + MultiSectionCapacitor.class); + ``` +2. Confirm `ClassAliasingMapper` wins over class lookup for a dotted element name. If it does not, + fall back to restoring a `@Deprecated` `ElectrolyticCanCapacitor` that extends + `MultiSectionCapacitor` and carries no descriptor — invisible in the palette, resolvable by + XStream. +3. `sections` is dropped by the §5.2 mapper, so the file loads but renders four sections where it + said one. Decide whether that drift is acceptable or whether it needs a converter that maps + `sections` onto `value` length. **Recommend accepting it** — the old field disagreed with its own + `value` array, so there is no correct answer to preserve. +4. Verify by loading `282.diy`. A regression run is the only real check. + +**~2 h.** Gate the rest of Tier 1 behind this. + +### 6.2 Revive the three live dormant classes + +Per §3.1, `AirCoreInductor`, `BoxTrimmer` and `Loadline` are finished code behind a commented +annotation. `AirCoreInductor` needs `ComponentDescriptor`, `CreationMethod`, `IDIYComponent` and +`SimpleComponentTransformer` (the last lives in `diylc-core`) re-imported; the other two need their +own commented imports restored the same way. + +Each one: uncomment, restore imports, compile, check it appears in the palette and draws. `Loadline` +is the most interesting of the three — tube and transistor load-line plotting is a design feature +no other item here provides, and it is already written. + +Then decide `AxialAirCoreInductor`'s fate. It is a 70-line stub that extended `AbstractFilmCapacitor`, +which is wrong for an inductor. **Recommend deleting the file** and treating axial air-core as a +variant of a revived `AirCoreInductor` if it is wanted at all; a commented-out stub inheriting from +the wrong base is worse than nothing. + +**~3 h for three, plus a decision on the fourth.** + +### 6.3 Tube socket bases + +`TubeSocket.Base` covers B9A, OCTAL, B7G and B12C. `Jumbo4PinTubeSocket` is a separate class scoped +by its own description to 211/805/845. Missing bases the amp community uses constantly: + +| Base | Pins | Tubes | +|---|---|---| +| UX4 | 4 | 300B, 2A3, 45, 5U4G | +| UX5 | 5 | type 80, 807 | +| B8G Loctal | 8 | 7N7, 7F7 | +| B9D Magnoval | 9 | EL509, PL519 | + +UX4 is the valuable one and the jumbo socket does not substitute — wrong diameter, wrong spacing. + +The class is clean to extend: `updateControlPoints()` switches on `base` for pin count, spacing and +a `hasEmptySpace` flag, `getBody()` switches for body diameter, and `draw()` carries per-base +keyway branches. **Both switches end in `default: throw new RuntimeException`, so a constant added +without touching both crashes at runtime rather than failing to compile.** Add the constant, the +`Size` constants, and a case in each switch. + +One geometry wrinkle: UX4 and UX5 have two large filament pins and two or three small ones, so pin +diameter becomes per-pin where today it is per-socket. Scope that before committing to UX4. + +Dimensions must come from datasheets, not from this table. **~6-8 h for all four, UX4 first.** + +### 6.4 Datasheets for the uncovered high-usage parts + +Per §4.2 and D6, and **not** the capacitor work an earlier draft of this document proposed — that +ships already. The parts with neither mechanism, in placement order: + +| Class | Placements | What it needs | +|---|---|---| +| `TransistorTO92` | 363 | BC547/BC557, 2N3904/2N3906, J201, MPF102, BC108, with pinout per part | +| `DiodePlastic` | 324 | 1N4148, 1N400x, 1N5817 | +| `RadialCeramicDiskCapacitor` | 283 | NPO/X7R by value and voltage, with disc diameter | +| `PotentiometerPanel` | 195 | Alpha/Bourns 16 mm and 24 mm bodies by resistance and taper | + +`RadialCeramicDiskCapacitor` is the natural first one: it is a capacitor, so +`CapacitorDatasheetService` already knows its column layout, and it is the only capacitor class of +the six most-placed with no coverage at all. + +`TransistorTO92` is the most valuable and the most work, because a transistor datasheet needs a +pinout column that the capacitor format has no equivalent for — the existing E-C-B / E-B-C options +would have to become a datasheet column. Scope that before starting. + +Per §5.3 each one is an `applyModel` implementation plus a CSV. Keep every cell ASCII and every +value `"<number> <unit>"`. + +**~6-10 h each, `RadialCeramicDiskCapacitor` first.** + +### 6.5 Fix six malformed datasheet rows — **bug** + +Six of the 3,539 rows have a value cell the parser cannot read. Found by validating every row +against `parseCapacitorDatasheet`'s own logic: + +| File | Rows | Cell | Failure | +|---|---|---|---| +| `RadialElectrolytic.datasheet` | 4 (Elna Silmic II 4.7 uF at 25/35/50/63 V) | `4.7uF` | no space, so `split(" ")` yields one element and `capacitanceParts[1]` throws `ArrayIndexOutOfBoundsException` | +| `RadialFilmCapacitor.datasheet` | 2 (WIMA MKS-4 400 V) | `0.01µF A`, `0.01µF B` | latin-1 `0xb5` in a file read with the platform charset (§5.3), no space, plus an ` A`/` B` suffix no other row has; `parseCapacitance` throws `IllegalArgumentException` | + +User-visible effect: picking any of those six models from *Apply Model* throws instead of applying. + +Latent effect: the same cells break `DatasheetService.lookup`, whose `Double.parseDouble` runs over +every row in a `model|voltage` bucket. Three of the five affected buckets have the bad row first, so +the whole bucket would throw — **80 rows across 5 buckets, including all 41 WIMA MKS-4 400 V rows**. +That path has no production caller today (§5.3), so this is a trap set for whoever wires up +value-based auto-sizing, not a live fault. + +1. Rewrite the four `4.7uF` cells as `4.7 uF`. +2. Replace the two `0.01µF A` / `0.01µF B` cells with ASCII. The ` A`/` B` suffix looks like an attempt + to give one value two body sizes (10x3 at 7.5 mm pitch, 13x4 at 10 mm pitch) — a real need the + positional format cannot express, since model and voltage are the whole lookup key. **Recommend + splitting them into two models**, `WIMA MKS-4` and `WIMA MKS-4 (10mm)`, matching how + `Epcos MKT (5mm)` / `(7.5mm)` / `(10mm)` and the WIMA 2.5 mm/5 mm entries already encode pitch in + the model name. +3. Add this row validation as a test beside `FactoryVariantsTest` — same shape, same + collect-all-problems-then-assert style — so the next malformed cell fails the build. **This is + the part that matters**; the six fixes are typos, the missing validation is why they survived. + +**~3 h including the test.** + +### 6.6 Make factory building blocks reach existing users — **bug, and a prerequisite** + +Per §5.4, `importDefaultBlocks()` runs once per installation and never again, so every factory +building block this roadmap proposes would land only on fresh installs. D7 routes most composable +parts through that mechanism, so this is a prerequisite for §6.7, §7.5 and §8.9, not a nicety. + +Two ways to fix it, in `BuildingBlockManager`: + +**Recommended — drop the guard.** Delete the `DEFAULT_BLOCKS_IMPORTED_KEY` check and let the +existing merge run on every start. The merge body already skips any name the user has +(`if (!blocksMap.containsKey(entry.getKey()))`), so user blocks and user edits survive untouched and +new factory names simply appear. Leave the flag being written so a downgrade still sees it. + +One behavioural cost, which should be a deliberate choice rather than a surprise: a user who +*deletes* a factory block gets it back on the next launch. Mitigate by recording deleted factory +names in config if that turns out to annoy anyone — not before. + +**Thorough — mirror the variant watermark.** Copy `VariantManager.importDefaultVariants()`: a +`defaultBlocksImportDate` watermark plus a per-block `createdOn`. The obstacle is that `blocks.xml` +is a `Map<String, List<IDIYComponent<?>>>` and `BuildingBlock` — which *does* have a name field but +is not used in the stored format at all — carries no timestamp. Adding one means changing the shape +of the `BLOCKS_KEY` config value, which also holds the user's own blocks. That is a config-format +migration for a gain the simpler option already delivers. + +Take the recommended option. Extend `BuildingBlockManagerTest` (8 tests, `diylc-core`) with a case +asserting that a second `importDefaultBlocks()` on a config that already has the flag set still +picks up a newly added default name. **~3 h.** + +### 6.7 Ship the first factory building blocks + +Data, not code, and per D7 this is where composable parts go. Each is authored in the app and lifted +into `blocks.xml`. Gated on §6.6. + +| Block | Built from | Why | +|---|---|---| +| Push-pull pot | `PotentiometerPanel` + `MiniToggleSwitch` (DPDT) | 12 files, 15 mentions — the highest-demand composable in the corpus | +| Transformer symbol | `TransformerCoil` x2 + `TransformerCore` | 15 files already hand-assemble exactly this; closes most of §8.1's transformer-symbol gap for free | +| Treble bleed | `Resistor` + `RadialFilmCapacitor` | Named network every guitar builder adds; trivial to author | +| Tube gain stage | `TubeSocket` + grid stopper + cathode R/C + plate R | 50 corpus files place a tube socket and then rebuild this by hand every time | +| Strat / Tele / LP harness | `guitar` pickups + pots + switch + jack | `guitar` components appear in only 8 of 425 files, which may be discoverability as much as demand | + +Author the push-pull pot first: it is the one with measured demand, and it is the worked example that +proves D7 before the rest follow. **~2-3 h each once §6.6 lands.** + +--- + +## 7. Tier 2 — new components, ranked by measured demand + +Parts that cannot be composed from what already ships, so D7 does not apply to them. §7.6 is +the exception and is kept only to record why it was declined. + +### 7.1 `Transformer` — the largest single gap + +Per D4: one class in `components.passive` extending `AbstractMultiPartComponent`, as +`AudioTransformer` does, with a `Type` enum of Power / Output / Choke, a bell-end or open-frame +body, and a configurable tap count per winding. + +What the absence costs today: no BOM line, no terminals, no netlist presence, and 15 projects +assembling one by hand out of `TransformerCoil` pieces. Every tube amp has two or three. + +Needs: `startTrackingContinuityArea` around the tap strip, mirroring a neighbouring multi-part +component exactly; an `IComponentTransformer` for rotation; `drawIcon()` hand-drawn for the +toolbox; `bomPolicy = SHOW_ONLY_TYPE_NAME` with the type and tap count in the variant label. + +A matching schematic symbol is §8.1. Ship the physical part first — the corpus demand is for +layouts. **~16-20 h.** + +### 7.2 `Speaker` + +Physical component plus a schematic symbol, 25 files and 45 hand-written mentions, and the only +item here that serves amp and hi-fi equally. Round frame with a configurable diameter, two +terminals, an impedance property. The symbol is a few hours; the physical part is a day. +**~10-12 h for both.** + +### 7.3 Chassis `Choke` + +21 files, 38 mentions. Once §7.1 exists this is largely its Choke type plus a single-winding body, +which is the main argument for building it as part of the `Transformer` class rather than beside it. +`RadialInductor` and `ToroidalInductor` are PCB parts and do not substitute. **~4 h if it rides on +§7.1, ~10 h standalone.** + +### 7.4 `Footswitch` + +`MiniToggleSwitch` already provides 3PDT electrically, but draws as a rounded-rectangle mini toggle. +A stomp switch is a round body with a 3x3 lug grid, and pedal layouts are built around that +geometry. ~20 files reference 3PDT, stomp or footswitch by hand. + +New class in `electromechanical`, reusing `ToggleSwitchType` so the netlist behaviour and the +existing `MiniToggleSwitchTests` patterns carry over. Add a momentary SPST variant for +non-latching relay bypass. Needs a `netlist/` test per §5.5. **~10-12 h.** + +### 7.5 `Photoresistor`, and `Vactrol` as a building block + +10 files, 15 mentions, and nothing discrete in the library — the only LDR class, `LDRSensorModule`, +is a dormant breakout board rather than the bare part. Blocks tremolo, optical compressors and Uni-Vibe-style circuits. + +Split by D7: + +- **`Photoresistor`** is a new component. It is a two-lead primitive with its own body and no + existing part to compose it from, so it has to be drawn. Extends `AbstractLeadedComponent`. +- **Vactrol** is then an `LED` plus a `Photoresistor`, which is a factory building block + (§6.7), not a class. A real vactrol is exactly those two parts in opaque heatshrink, so the + assembly is honest. + +**~5 h for the component, then an hour to author the block.** + +### 7.6 Push-pull pot — **declined as a component** + +Kept here as a record of the decision, because it is the case D7 exists for. + +12 files and 15 mentions make this the best-evidenced composable part in the corpus, and an earlier +draft of this roadmap proposed a `PushPullPotentiometer` class extending `AbstractPotentiometer`. +That is the wrong answer. A push-pull pot is a `PotentiometerPanel` and a DPDT `MiniToggleSwitch`, +both of which already draw correctly, already report the right terminals, and already have netlist +coverage — `MiniToggleSwitchTests` alone is 15 cases. A new class would re-derive all of it and add +a toolbox entry. + +It ships as a factory building block instead: §6.7, first in the list. + +The same reasoning retires the alternative that was also considered, a `switch` property on +`PotentiometerPanel`. Besides D7, it would change the control-point count of a class placed 195 +times in the corpus, which §5.2 forbids for existing files. + +--- + +## 8. Tier 3 — reasoned, not measured + +Per D2 these have no corpus evidence. Ordered by how confident the judgement is, not by cost. + +**8.1 Schematic symbol holes.** Thirty symbols with specific omissions: no speaker (§7.2), no +momentary/push-button switch (only `SwitchLatchingSymbol`), no single transformer symbol with taps +(only the `TransformerCoil` + `TransformerCore` primitives), no tetrode or beam-power tube and no +dual-triode envelope for a 12AX7, no LDR, opto-isolator, thermistor, varistor, relay or SCR/Triac, +and no crystal symbol although a physical `CrystalOscillator` ships. `GroundSymbol` offers only +Default and Triangle with no chassis-versus-earth distinction, which matters for amp safety +drawings. The `IC` symbol's 3- and 5-contact forms do serve as an op-amp triangle. Each is a few +hours. + +Two of these are not component work at all. The **transformer symbol** is `TransformerCoil` twice +plus `TransformerCore`, so per D7 it is a factory building block (§6.7) and needs no class. The +**opto-isolator symbol** is likewise an `LEDSymbol` facing a photoresistor symbol, once the latter +exists. That leaves the **momentary switch** as the one genuinely missing primitive here, and the +first worth drawing. + +**8.2 Hi-fi connectors.** XLR (3-pin, male and female, chassis and PCB), binding post, banana, +speakON, 5-pin DIN, BNC. Zero corpus hits, high confidence from domain knowledge, and the weakest +domain in the library — there is no hi-fi category at all. + +**8.3 Speaker crossover parts.** Reviving `AirCoreInductor` in §6.2 is the prerequisite and the +cheapest hi-fi win available. Beyond it: a mains toroid (`ToroidalInductor` is an inductor, not a +transformer with taps) and non-polar film caps at crossover values. + +**8.4 Modern power packages.** No TO-247, TO-264 or TO-3P, so a current power amp cannot be drawn; +vintage TO-3 ships. Plus a bolt-on heatsink — `HeatSinkResistor` is a resistor, not a heatsink. + +**8.5 Pickups not covered.** Jazzmaster (a wide soapbar, geometrically distinct from the P-90), +Danelectro lipstick, Rickenbacker toaster, gold foil, piezo/under-saddle. All zero corpus hits, +which is why they sit below the Tier 2 guitar item despite looking more visible. + +**8.6 Connected board positions.** The `EyeletBoard` problem from §4.4: selectable eyelet and +turret positions that carry node names and reach the netlist. Benefits both boards and would +retire the discrete-turret workaround in 40 projects. Design question, not a component. + +**8.7 Mains safety parts.** NTC inrush thermistor, MOV, X and Y capacitors. One corpus file +mentions NTC. + +**8.8 Reverb tank.** Accutronics-style, 6 files. Simple geometry, narrow audience. + +**8.9 Dual-concentric pot.** Jazz Bass, '62 Strat, Rickenbacker. Zero corpus hits, and per D7 not a +component: two `PotentiometerPanel` instances sharing a position is a factory building block, so it +joins the §6.7 list rather than this one once someone asks for it. + +--- + +## 9. Sequencing + +| Step | Items | Rough effort | +|---|---|---| +| 1 | §6.5 six malformed datasheet rows, plus the validation test | 3 h | +| 2 | §6.1 alias fix, verified by regression run | 2 h | +| 3 | §6.6 factory building block import guard | 3 h | +| 4 | §6.7 push-pull pot block, as the D7 worked example | 3 h | +| 5 | §6.2 revive three, decide on the fourth | 3 h | +| 6 | §6.3 UX4, then UX5 / B8G / B9D | 6-8 h | +| 7 | §6.4 datasheets, `RadialCeramicDiskCapacitor` first | 6-10 h each | +| 8 | §6.7 remaining blocks: transformer symbol, treble bleed, tube gain stage, harnesses | 2-3 h each | +| 9 | §7.1 `Transformer`, with §7.3 Choke as its third type | 20-24 h | +| 10 | §7.2 Speaker, physical plus symbol | 10-12 h | +| 11 | §7.4 Footswitch | 10-12 h | +| 12 | §7.5 `Photoresistor`, then the Vactrol block | 6 h | + +Steps 1-4 are about a day and a half: both known bugs cleared, the building block channel unblocked, +and the highest-demand composable shipped as data rather than as a class. Step 9 is the one that +matters most. + +Three items that looked like components when this roadmap started are now data (§6.7, §7.6, §8.9), +and one is split into a primitive plus a block (§7.5). That is D7 working as intended; expect the +same question of anything added to Tier 2 later. + +Figures are estimates for scoping, not commitments, and every dimension in this document is an +estimate until it is checked against a datasheet (§6.3, §6.4).