Files
diy-layout-creator/diylc/docs/plans/audio-coverage-roadmap.md
T
2026-11-06 09:39:07 +01:00

33 KiB

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 <text>, <title>, <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 List<BuildingBlock>, each with its name, components (class="list") and createdOn, using the diylc package alias (aliasPackage("diylc", "org.diylc.components")) for the components. It is merged into the user's own blocks (the unchanged Map<String, List<IDIYComponent<?>>> under buildingBlocks) by name.

Three ship today, all createdOn 2026-01-01: 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. Running with -Dorg.diylc.WriteStaticBlocks=true writes the saved block in the bundled shape, dated now. Per D7 this is the default route for anything composable.

The import runs on every start and mirrors VariantManager.importDefaultVariants():

  • A block is imported when its createdOn is after the defaultBlocksImportDate watermark (LocalDateTime.MIN when absent) and the user has no block of that name. A same-named user block is never overwritten or duplicated.
  • A withdrawn block is a stub with only name and deletedOn; it is removed from the user's blocks and recently used list when deletedOn is after the watermark.
  • The watermark becomes the latest createdOn/deletedOn in the file, never the clock, and never moves backwards, so the order in which versions are launched does not matter.
  • defaultBlocksImported is still written so that versions before the watermark do not import.

Because configs from those versions have no watermark, the first start of this version offers every factory block once, and a user who deleted one gets it back once.

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

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 and §6.5 are correctness fixes and should go first regardless of the rest.

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:
    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 — done

Factory blocks carry createdOn/deletedOn behind a defaultBlocksImportDate watermark; see §5.4.

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; each gets a createdOn later than the newest one already in the file.

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.


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.7 push-pull pot block, as the D7 worked example 3 h
4 §6.2 revive three, decide on the fourth 3 h
5 §6.3 UX4, then UX5 / B8G / B9D 6-8 h
6 §6.4 datasheets, RadialCeramicDiskCapacitor first 6-10 h each
7 §6.7 remaining blocks: transformer symbol, treble bleed, tube gain stage, harnesses 2-3 h each
8 §7.1 Transformer, with §7.3 Choke as its third type 20-24 h
9 §7.2 Speaker, physical plus symbol 10-12 h
10 §7.4 Footswitch 10-12 h
11 §7.5 Photoresistor, then the Vactrol block 6 h

Steps 1-4 are about a day and a half: both known bugs cleared and the highest-demand composable shipped as data rather than as a class. Step 8 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).