Files
diy-layout-creator/diylc/docs/plans/component-pinout-catalog.md
T
Bane Stojkovic bc5f203e21 Ai edit (#1065)
* initial

* Updates

* improvements

* split netlist from analysis

* coordinates in grid

* Update IChatbotAPI.java

* cleanup

* cleanup

* various improvements

* Datasheet model support

* Include pinouts in component catalog

* Update update.xml

* regenerate catalog

* Zooming out until the drawing is smaller than the window made the toolbar above the canvas grow taller and squeeze the canvas

* refresh

* edit improvements

* refine wire handling

* CR

* catalog gen improvement

* improvements

* Rotating and mirroring the selection with 'Sticky Points' enabled now keeps the circuit connected

* fix version reading

* set dimensions and mode in any order

* add AINote

* add aiNote

* board notes added

* ai notes

* ai notes

* ai notes

* led minor code improvement and ai note

* single coil fix. guitar component notes

* add support for replace operation

* Update pom.xml

* switch notes

* ai notes for pickups

* stingray

* reconnect operation support

* ai edit code refactor

* Cliff jack node name fixes

* ai notes

* ai notes

* ai notes

* netlist improvement

* simplify

* solder lug improvements

* tag strip enhancement

* regression fixes

* ai notes

* battery snap improvement

* more sample questions

* 'IEC Socket', 'Plastic DC Jack', 'Multi-Section Capacitor' and 'Mini Relay' terminals are now labeled by function

* node improvement

* Regression data consolidation

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-10-30 09:32:03 +01:00

314 lines
17 KiB
Markdown

# Component Pinout Catalog
Teaching the LLM what terminals a component type has before any instance of it exists.
## 1. Goal
`ComponentCatalogGenerator` produces `catalog_full.json` and `catalog_index.json`
offline. The files are uploaded once to the diy-fever.com backend, which reads
out only the entries it needs when serving a request. Each entry today carries
the type's identity, its category, its creation method and its editable
properties.
It carries nothing about terminals. For components already on the canvas the
model gets terminals from `AiProjectBuilder`, which emits an `AiTerminal` per
sticky control point with its index, its node name and its position. For a type
the model has never seen instantiated there is no equivalent, so when it writes
an `AiEditOperation` with `fromTerminal: "SW1.3"` it is guessing.
The guess is only safe for the trivial cases. A resistor is always two pins. An
axial electrolytic is `+` and `-` when polarized and `1`/`2` when not. A mini
toggle switch is anywhere from two to fifteen lugs depending on its `Type`, and
which lug does what changes with it.
This document plans a `pinout` block on every catalog entry that needs one,
derived from the code rather than declared by hand.
## 2. What the code actually does
The numbers below come from a probe run over the whole library: instantiate
every discovered `ComponentType`, sweep every non-read-only enum and boolean
editable property one value at a time, and record the ordered list of sticky
control points with their node names.
| Measurement | Value |
| --- | --- |
| Component types discovered by `ComponentProcessor` | 144 |
| `@ComponentDescriptor` annotations in the source tree | 153 |
| Types whose sticky pinout changes with at least one property | 51 |
| Types with more than one independent driver | 8 |
| Types implementing `ISwitch` | 12 |
The first sweep covered enums and booleans only and found 49. Adding the integer sweep of §2
brought in `TagStrip` and a second driver on `TerminalStrip`.
The eight multi-driver types are `DIL_IC` (pin count and numbering scheme),
`SIL_IC`, `SubminiTube` (lead count, folded, pin arrangement),
`SwitchLatchingSymbol` (poles and configuration), `AudioTransformer` (two
booleans), and the tube symbols `TriodeSymbol`, `PentodeSymbol` and
`DuoDiodeSymbol` (heaters and directly-heated, plus suppressor grid on the
pentode).
Four facts from the probe shape everything below.
**Sticky is the only filter that matches the project view.** Raw
`getControlPointCount()` over-counts. The moveable label anchor on most leaded
components is a control point, so a plain resistor reports three points, not
two. `AiProjectBuilder.mapComponent` already skips non-sticky points, and the
catalog must use the identical filter or the indices in the catalog will not be
the indices the model sends back in a terminal reference. Filtering on sticky
also removes the false driver "Moveable Label", which otherwise shows up on
thirty-two types that have no electrical variation at all.
**Indices are not contiguous.** `RotarySwitchOpen` and `RotarySwitchSealed`
have a non-sticky point at index 0, so their first terminal is index 1.
`TubeSocket` is the same. The pinout must carry explicit indices, not a bare
ordered list.
**Some drivers are unbounded integers.** `TerminalStrip.getTerminalCount()`,
`TagStrip.getTerminalCount()`, `PinHeader.getRowCount()` and
`PinHeader.getColumnCount()` are plain `int` editable properties that call
`updateControlPoints()`. There is no enum to enumerate. Any design that assumes
a finite value domain silently omits these.
**Some node names are unusable as pin labels.** See §6.
## 3. Design decisions
| # | Decision | Choice | Rationale |
|---|---|---|---|
| D1 | How drivers are identified | Derived by probing, not declared by annotation | An annotation is correct in spirit but rots. There is no test that fails when a new component ships without it, and 8 of 49 types need more than one annotation anyway. Derivation re-reads the truth on every build. |
| D2 | Which points count as terminals | Sticky control points only | Identical filter to `AiProjectBuilder.mapComponent`, so catalog indices and project indices agree. |
| D3 | Property domains swept | Enum, `Boolean`/`boolean`, and integer properties sampled over a small range | Covers all 49 observed cases plus the four integer drivers the enum-only sweep misses. |
| D4 | Emission shapes | `fixed`, `variants`, `parametric` | Three genuinely different situations. Forcing all three into one table is what makes the file large. |
| D5 | Regular label runs | Emitted as a rule, not a table | `DIL_IC` is 24 pin counts times 5 numbering schemes. Its labels are a formula, so it collapses to one line instead of 120 variants. Irregular label sets, such as the 15 `TransistorPinout` values, stay a table. |
| D6 | Multiple drivers | Cartesian product, deduplicated by signature, capped at 64 combinations | Product order is small once D5 removes the regular cases. The cap is a guard, not an expected path. |
| D7 | Switch behaviour | A `switching` block per pinout variant, mirroring `AiSwitch` | Knowing a DPDT-on-on-on has six lugs is useless without knowing which lugs bridge in which position. The same probe produces it from `ISwitch.arePointsConnected`. |
| D8 | Unusable node names | Fixed at source in the eight offending components | Preferred over a heuristic in the generator. Changes netlist text output, so it needs a regression run. See §6. |
| D9 | Escape hatch | `@EditableProperty(pinoutDriver = ...)` with `AUTO`, `FORCE`, `EXCLUDE` | Derivation will be wrong somewhere. The annotation stays in the design as an override, not as the mechanism. Default `AUTO` means nothing has to be touched. |
| D10 | Preventing rot | A snapshot test in `diylc-library` asserting the derived driver set per type | This is the safety the annotation was really buying. Adding a component with an undeclared new driver fails the build until the snapshot is regenerated. |
| D11 | Where the analyzer lives | `diylc-core`, beside `ComponentCatalogGenerator` | It touches only `IDIYComponent`, `PropertyWrapper` and `ComponentProcessor`. No concrete component type is referenced, so the core boundary holds. |
| D12 | Boards | No pinout block at all | `AbstractBoard.isControlPointSticky` returns `false` for every index, so the sticky filter yields zero terminals and the entry is simply omitted. Components are placed on a board by coordinate, not by terminal. |
### Explicitly out of scope
- Relative pin geometry in the catalog. The model currently places a multi-pin
component by a single anchor and then wires to terminals by reference, which
`AiEditScriptEditor` resolves against the real instance. Pin offsets would
help it reason about spacing, but that is a separate change.
- The datasheet axis. Applying a datasheet model to an electrolytic can flip
`Polarized` and therefore the pinout, because `applyModel` calls
`setPolarized(!d.getNonPolarized())`. The pinout block describes the
`Polarized` property honestly; it does not enumerate which datasheet rows set
it. Five component types are affected.
- Changing how the server prompts with this data.
## 4. The three shapes
A description of one set of terminals carries exactly one of three key groups, so each key has a
stable type wherever it appears:
| Keys | Meaning |
| --- | --- |
| `pins` | An explicit list of `{ i, label }`, used whenever the labels follow no rule or the indices are not contiguous. |
| `pinCount` plus `labels` | A count and a rule, where `labels` is `sequential` or `none`. |
| `pinCountExpression` plus `labels` plus `pinCountSamples` | A formula over numeric properties, with a few worked values as evidence. |
An `ISwitch` also carries `switching`, which always names the `positionCount` and lists `positions`
when there are few enough of them to be worth reading.
### Fixed
65 of 144 types. One description, no drivers, emitted at the root of `pinout`.
```json
"pinout": {
"pins": [ { "i": 0, "label": "A" }, { "i": 1, "label": "K" } ]
}
```
### Variants
Finite drivers. One entry per combination.
This is what the mini toggle switch actually produces, abbreviated to two of its sixteen types:
```json
"pinout": {
"drivers": ["Type"],
"variants": [
{
"when": { "Type": "SPST" },
"pinCount": 2,
"labels": "sequential",
"switching": {
"positionCount": 2,
"positions": [
{ "name": "ON1", "connected": [[0, 1]] },
{ "name": "ON2", "connected": [] }
]
}
},
{
"when": { "Type": "DPDT" },
"pinCount": 6,
"labels": "sequential",
"switching": {
"positionCount": 2,
"positions": [
{ "name": "ON1", "connected": [[0, 1], [3, 4]] },
{ "name": "ON2", "connected": [[1, 2], [4, 5]] }
]
}
}
]
}
```
`when` carries enum constant names, matching the `possibleValues` already
emitted for the property and matching what `AiEditScriptEditor.parsePropertyValue`
accepts.
### Parametric
A numeric property drives the count. The generator samples it, fits a formula, and verifies the
fit against every sample before emitting it. A driver counts as numeric when it is an `int`
property, or an enum whose constants read as numbers either through `toString` or through a
`getValue` or `getCount` accessor. That last case is what collapses the dual inline package from
120 combinations to five.
```json
"pinout": {
"drivers": ["Center Terminal", "Terminals"],
"variants": [
{
"when": { "Center Terminal": "false" },
"pinCountExpression": "2 * Terminals",
"labels": "custom",
"pinCountSamples": { "10": 20, "11": 22, "12": 24 },
"example": { "when": { "Terminals": "10" }, "pins": [ { "i": 0, "label": "A1" } ] }
}
]
}
```
One numeric driver is fitted to `a * n + b`, two to a product or a sum. Anything else, or a fit
that fails verification, falls back to enumerating the drivers instead. `labels` takes
`sequential` when the labels are `1..N`, `none` when every label is null, and `custom` otherwise,
and a custom rule brings along one worked `example` because there is no array to show.
### The record model
The analyzer builds records in `org.diylc.plugins.chatbot.model`, beside the ones that already
describe a project to the model, and Jackson serializes them. `AiPinout` holds the drivers and
either one `AiTerminals` or a list of `AiPinoutVariant`, each of which pairs a `when` map with its
own `AiTerminals`. `AiTerminals` carries the three key groups above, and its three static factories
are the only way to build one, so a description cannot end up claiming to be two shapes at once.
`AiPinLabels` replaces the label rule strings, and `AiPin`, `AiSwitching`, `AiSwitchingPosition` and
`AiPinoutExample` cover the rest.
`AiTerminals` is composed into `AiPinout` and `AiPinoutVariant` through `@JsonUnwrapped`, which
keeps the model free of duplicated components while leaving the emitted JSON flat.
## 5. Implementation
Four pieces, in order.
1. **`PinoutAnalyzer` in `org.diylc.plugins.chatbot.service`.** Entry points
`analyze(ComponentType, ComponentProcessor)` and `analyze(Class, ComponentProcessor)`, both
returning the record model described at the end of §4. Internally: read the baseline terminals; sweep candidate properties
one at a time to find drivers; split them into numeric and finite; walk the product of the
finite ones; fit a formula over the numeric ones, falling back to enumeration when nothing
fits; attach switching tables for `ISwitch`. Every instantiation and every property write is
wrapped, and a failure downgrades that property to "not a driver" rather than failing the
entry. Drivers are sorted by name, because `extractProperties` follows `Class.getMethods()` and
the JVM makes no promise about that order.
2. **`ComponentCatalogGenerator.buildFullEntry`** gains a call to the analyzer
and puts the result under `pinout`. Two existing defects get fixed while the
method is open: the index loop dereferences `comp` outside its own null
check, and the `VERSION` constant reads `6.2.0` against a pom at `6.4.0`.
3. **`pinoutDriver` on `EditableProperty`** as a new element with default
`AUTO`. Adding an element with a default is safe for a runtime-retained
annotation and touches no existing component.
4. **The node name fixes** of §6, in `diylc-library`.
### Bounds
Three caps keep one component from swamping the file. At most 64 variants, dropping drivers
smallest domain first and naming the dropped ones in `driversHeldAtDefault`. At most four rows of
formula evidence, since the full domain of a numeric enum is already published as the property's
possible values. At most 24 switch positions written out in full, which matters because a DIP
switch reports two to the power of its switch count: without the cap its entry alone came to
1.27 MB, against 98 KB for all 144 types together afterwards.
## 6. Node names that need fixing
`getControlPointNodeName` serves the netlist, and `Node.toString()` already
renders `component.getName() + "." + displayName`. Eight components prepend the
component name a second time or return a hard-coded class name, so a netlist
node reads `V1.V1.+` today.
| Component | Returns now | Should return |
| --- | --- | --- |
| `BatterySymbol` | `getName() + "." + "+"` | `+` / `-` |
| `BulbSchematicSymbol` | same | `+` / `-` |
| `BuzzerSymbol` | same | `+` / `-` |
| `CurrentSourceSymbol` | same | `+` / `-` |
| `VoltageSourceSymbol` | same | `+` / `-` |
| `PhonoJackSymbol` | `getName() + "." + index` | `Tip` / `Sleeve`, and `Ring` when the type is stereo |
| `TerminalStrip` | `"TerminalStrip" + index` | `A1`…`An` for the front row, `B1`…`Bn` for the row it is bonded to, `C1`…`Cn` for the center row |
| `TagStrip` | `"TerminalStrip" + index` | nothing, the override is removed so the inherited `1`…`N` applies |
`PhonoJackSymbol` follows `ClosedJack1_4` and `OpenJack1_4`, which already name their points this
way. `TagStrip` had a verbatim copy of `TerminalStrip`'s string despite having a single row, so
deleting its override is both the smallest fix and the right one.
This is a real defect independent of the catalog, but it changes netlist text
output for any project using those parts. It needs a regression suite run and
regeneration of the affected reference netlists, which is why it is listed last
in §5 and can ship separately.
`PCBTerminalBlock` is deliberately excluded. It returns null until the user
supplies node names in a free-text property, and null correctly means "not a
named node". The catalog emits `"labels": "none"` for it.
## 7. Testing
- **Unit, `diylc-core`.** `PinoutAnalyzerTest` runs the analyzer against thirteen hand-built stub
components: no drivers, a label anchor that must not count as a terminal, no sticky points at
all, indices that do not start at zero, unnamed terminals, an enum driver, a boolean driver, two
drivers combined, an integer driver, both annotation overrides, a setter that rejects one of its
own values, and a switch.
- **Snapshot, `diylc-library`.** `PinoutDriverSnapshotTests` runs the analyzer over every
`ComponentType` and compares the derived driver names against
`src/test/resources/pinout-drivers.txt`, one line per type. Per D10 this is what catches a new
component with an undeclared pinout driver. It also pins the 51 known varying types so a
refactor cannot quietly drop one. On failure it writes the regenerated file to `target` and
names it, so accepting a real change is a copy.
- **Regression suite.** Required for §6 only. The analyzer itself never runs at
drawing time and cannot affect rendering or netlist output.
## 8. Running the generator
`ComponentProcessor` finds components by scanning the runtime classpath for
`org.diylc.components`, and those classes ship in `diylc-library`. The generator's own class sits
in `diylc-core`, which does not depend on `diylc-library`, so launching it against the
`diylc-core` module scans an empty package set and writes a catalog with no components in it. That
is a trap worth knowing about, because nothing about the failure looked like a failure.
Three things now stand in the way of hitting it again. The generator refuses to write an empty
catalog and says what to fix. `.run/ComponentCatalogGenerator.run.xml` launches it against the
`diylc-library` module, writing into the `diylc` directory where the committed catalogs live.
`ComponentCatalogGeneratorTest` moved from `diylc-core` to `diylc-library`, where the classpath
holds real components, and it now asserts that the catalog comes out populated and that a couple
of known entries carry the pinout they should.
The generator also sorts editable properties the way the property editor does, using
`ComparatorFactory.getDefaultPropertyComparator`. Property extraction follows
`Class.getMethods()`, whose order the JVM does not promise, so without that every regeneration
produced a large meaningless diff in a file that is committed. Two consecutive runs are now
byte-identical.