.claude/skills/handler-pattern-design/SKILL.md
# Handler Pattern Design Use this skill when creating new manufacturer handlers or refactoring existing ones. This skill codifies proven patterns, anti-patterns, and best practices from 67 production handlers. ## Core Patterns ### 1. Set.of() vs HashSet in getSupportedTypes() **Pattern: Always use immutable Set.of() instead of mutable HashSet.** **Why:** Immutable collections prevent accidental modification and are more efficient. ```java // ✅ CORRECT: Use Set.of() @Override public Set<Com
npx skillsauth add Cantara/lib-electronic-components .claude/skills/handler-pattern-designInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Use this skill when creating new manufacturer handlers or refactoring existing ones. This skill codifies proven patterns, anti-patterns, and best practices from 67 production handlers.
Pattern: Always use immutable Set.of() instead of mutable HashSet.
Why: Immutable collections prevent accidental modification and are more efficient.
// ✅ CORRECT: Use Set.of()
@Override
public Set<ComponentType> getSupportedTypes() {
return Set.of(
ComponentType.MICROCONTROLLER,
ComponentType.MICROCONTROLLER_ST,
ComponentType.MOSFET,
ComponentType.MOSFET_ST,
ComponentType.VOLTAGE_REGULATOR
);
}
// ❌ WRONG: Don't use HashSet
@Override
public Set<ComponentType> getSupportedTypes() {
Set<ComponentType> types = new HashSet<>();
types.add(ComponentType.MICROCONTROLLER);
types.add(ComponentType.MOSFET);
return types; // Mutable set can be modified!
}
Technical debt: 34 handlers still use HashSet and need migration to Set.of().
Pattern: Register patterns for BOTH the base type AND the manufacturer-specific type.
Why: Users may search for either generic "MOSFET" or specific "MOSFET_INFINEON". Both must work.
@Override
public void initializePatterns(PatternRegistry registry) {
// ✅ CORRECT: Register both base and specific types
registry.addPattern(ComponentType.MOSFET, "^IRF[0-9]+.*"); // Base type
registry.addPattern(ComponentType.MOSFET_INFINEON, "^IRF[0-9]+.*"); // Specific type
registry.addPattern(ComponentType.OPAMP, "^LM358.*");
registry.addPattern(ComponentType.OPAMP_TI, "^LM358.*");
}
// ❌ WRONG: Only registering manufacturer-specific type
registry.addPattern(ComponentType.MOSFET_INFINEON, "^IRF[0-9]+.*");
// Users searching for ComponentType.MOSFET will get 0 results!
TIHandler example (lines 366-376):
for (Map.Entry<String, ComponentSeriesInfo> entry : COMPONENT_SERIES.entrySet()) {
ComponentSeriesInfo info = entry.getValue();
// Register primary type
registry.addPattern(info.primaryType, info.pattern);
// Register base component type
registry.addPattern(info.primaryType.getBaseType(), info.pattern);
// Register additional types if any
for (ComponentType additionalType : info.additionalTypes) {
registry.addPattern(additionalType, info.pattern);
}
}
Pattern: Use matchesForCurrentHandler() to prevent cross-handler false matches.
Why: Using patterns.matches() or patterns.getPattern() searches ALL handlers' patterns, causing false positives when another manufacturer shares a prefix.
@Override
public boolean matches(String mpn, ComponentType type, PatternRegistry patterns) {
if (mpn == null || type == null) return false;
String upperMpn = mpn.toUpperCase();
// 1. Do specific checks for your manufacturer first
if (upperMpn.startsWith("STM32")) {
return type == ComponentType.MICROCONTROLLER || type == ComponentType.MICROCONTROLLER_ST;
}
// 2. ✅ CORRECT: Use matchesForCurrentHandler() for fallback
return patterns.matchesForCurrentHandler(upperMpn, type);
// 3. ❌ WRONG: Don't use patterns.matches() - searches ALL handlers!
// return patterns.matches(upperMpn, type); // Cross-handler contamination!
}
NXPHandler example (line 180):
// Use handler-specific patterns for other matches (avoid cross-handler false matches)
return patterns.matchesForCurrentHandler(upperMpn, type);
Critical bug example (PR #89): AtmelHandler used patterns.matches() and incorrectly matched PIC microcontrollers (Microchip), STM32 (ST), and NXP parts as Atmel because it returned true for base MICROCONTROLLER type from other handlers' patterns.
Pattern: Always normalize MPN to uppercase BEFORE position-based extraction.
Why: Position-based extraction with charAt() breaks if hyphens or special chars are present. MPN normalization (via MPNUtils.normalize()) handles this, but at minimum always uppercase.
@Override
public String extractPackageCode(String mpn) {
if (mpn == null || mpn.isEmpty()) return "";
// ✅ CORRECT: Normalize to uppercase first
String upperMpn = mpn.toUpperCase();
// STM32F103C8T6 → package is second-to-last char
if (upperMpn.startsWith("STM32") && upperMpn.length() >= 2) {
char packageChar = upperMpn.charAt(upperMpn.length() - 2); // 'T'
return switch (packageChar) {
case 'T' -> "LQFP";
case 'H' -> "BGA";
case 'U' -> "VFQFPN";
default -> String.valueOf(packageChar);
};
}
// ❌ WRONG: Using mpn directly without normalization
// char packageChar = mpn.charAt(mpn.length() - 2); // Fails if mpn is lowercase!
return "";
}
AbstractManufacturerHandler helpers (lines 66-94):
// Use centralized PackageCodeRegistry
protected String extractPackageCode(String mpn) {
if (mpn == null || mpn.isEmpty()) return "";
String upperMpn = mpn.toUpperCase();
// Try hyphen-separated suffix first (e.g., "ATMEGA328P-PU" -> "PU")
String suffix = extractSuffixAfterHyphen(upperMpn);
if (!suffix.isEmpty() && PackageCodeRegistry.isKnownCode(suffix)) {
return PackageCodeRegistry.resolve(suffix);
}
// Try common suffix patterns at end of MPN
suffix = extractTrailingSuffix(upperMpn);
if (!suffix.isEmpty() && PackageCodeRegistry.isKnownCode(suffix)) {
return PackageCodeRegistry.resolve(suffix);
}
return "";
}
TIHandler override (lines 436-467): Overrides for manufacturer-specific package extraction but still calls helper methods.
Pattern: Check longer/more specific patterns BEFORE shorter/generic patterns.
Why: Prevents false matches. "MPXV5010" should match "MPXV" series, not "MPX" series.
@Override
public String extractSeries(String mpn) {
if (mpn == null || mpn.isEmpty()) return "";
String upperMpn = mpn.toUpperCase();
// ✅ CORRECT: Check specific prefixes first, then generic
if (upperMpn.startsWith("MPXV")) return "MPXV Pressure Sensor"; // Specific
if (upperMpn.startsWith("MPXA")) return "MPXA Pressure Sensor"; // Specific
if (upperMpn.startsWith("MPX")) return "MPX Pressure Sensor"; // Generic last
// ❌ WRONG: Generic first - "MPXV5010" would incorrectly return "MPX"
// if (upperMpn.startsWith("MPX")) return "MPX Pressure Sensor";
// if (upperMpn.startsWith("MPXV")) return "MPXV Pressure Sensor";
return "";
}
NXPHandler example (lines 355-358):
// Sensors - CHECK SPECIFIC PREFIXES FIRST!
if (upperMpn.startsWith("MPXV")) return "MPXV Pressure Sensor";
if (upperMpn.startsWith("MPXA")) return "MPXA Pressure Sensor";
if (upperMpn.startsWith("MPX")) return "MPX Pressure Sensor"; // Generic last
AbstractManufacturerHandler helper (lines 135-153):
@Override
public String extractSeries(String mpn) {
if (mpn == null || mpn.isEmpty()) return "";
String upperMpn = mpn.toUpperCase();
// Default: extract prefix + first numeric sequence
int firstDigit = findFirstDigitIndex(upperMpn, 0);
if (firstDigit < 0) {
return upperMpn; // No digits, return whole thing
}
int endOfDigits = firstDigit;
while (endOfDigits < upperMpn.length() && Character.isDigit(upperMpn.charAt(endOfDigits))) {
endOfDigits++;
}
return upperMpn.substring(0, endOfDigits);
}
CRITICAL: Test files MUST NOT be in the manufacturers package.
Why: Test-classes directory shadows main-classes directory. If handler tests are in no.cantara.electronic.component.lib.manufacturers, the test-classes directory will be found first during classpath scanning, causing ManufacturerHandlerFactory to find 0 handlers (catastrophic failure).
✅ CORRECT Package:
no.cantara.electronic.component.lib.handlers
└── TIHandlerTest.java
└── STHandlerTest.java
└── NXPHandlerTest.java
❌ WRONG Package (NEVER DO THIS):
no.cantara.electronic.component.lib.manufacturers
└── TIHandlerTest.java // CLASSPATH SHADOWING - breaks ManufacturerHandlerFactory!
Test file structure:
package no.cantara.electronic.component.lib.handlers; // ✅ handlers, not manufacturers
import no.cantara.electronic.component.lib.manufacturers.TIHandler; // Import handler from manufacturers package
import org.junit.jupiter.api.*;
class TIHandlerTest {
private static TIHandler handler;
private static PatternRegistry registry;
@BeforeAll
static void setUp() {
handler = new TIHandler(); // Direct instantiation
registry = new PatternRegistry();
handler.initializePatterns(registry);
}
}
Problem: ComponentType ↔ ComponentManufacturer ↔ Handler circular dependency can cause ExceptionInInitializerError.
Solution: Use direct instantiation in @BeforeAll.
@BeforeAll
static void setUp() {
// ✅ CORRECT: Direct instantiation - simple and avoids circular dependency
handler = new TIHandler();
registry = new PatternRegistry();
handler.initializePatterns(registry);
}
// ❌ WRONG: Using MPNUtils lookup triggers ComponentManufacturer loading
// handler = (TIHandler) MPNUtils.getManufacturerHandler("LM358");
// This can cause ExceptionInInitializerError if ComponentManufacturer enum isn't loaded yet
Alternative (if direct instantiation fails): Use MPNUtils.getManufacturerHandler("MPN") if direct instantiation causes issues.
Test isolation gotcha: Running a single handler test may fail while full suite passes. This is due to class loading order - other tests initialize classes first. If single test fails with initialization errors, run full suite to verify.
Pattern: Group related tests with nested classes for better organization.
class TIHandlerTest {
@Nested
@DisplayName("Op-Amp Detection")
class OpAmpTests {
@ParameterizedTest
@DisplayName("Should detect LM358 variants as OPAMP_TI")
@ValueSource(strings = {"LM358", "LM358N", "LM358D", "LM358PW"})
void shouldDetectLM358Variants(String mpn) {
assertTrue(handler.matches(mpn, ComponentType.OPAMP_TI, registry));
assertTrue(handler.matches(mpn, ComponentType.OPAMP, registry));
}
}
@Nested
@DisplayName("Voltage Regulator Detection")
class VoltageRegulatorTests {
@ParameterizedTest
@CsvSource({
"LM7805, VOLTAGE_REGULATOR_LINEAR_TI",
"LM7812, VOLTAGE_REGULATOR_LINEAR_TI",
"UA7805, VOLTAGE_REGULATOR_LINEAR_TI"
})
void shouldDetect78xxRegulators(String mpn, String expectedType) {
ComponentType type = ComponentType.valueOf(expectedType);
assertTrue(handler.matches(mpn, type, registry));
}
}
@Nested
@DisplayName("Package Code Extraction")
class PackageCodeTests {
// ...
}
}
Benefits:
Pattern: Use @ParameterizedTest for testing multiple MPNs with same logic.
// Simple list of MPNs
@ParameterizedTest
@ValueSource(strings = {"LM358", "LM358N", "LM358D", "LM358PW"})
void shouldDetectLM358Variants(String mpn) {
assertTrue(handler.matches(mpn, ComponentType.OPAMP_TI, registry));
}
// MPN + expected result pairs
@ParameterizedTest
@CsvSource({
"LM7805CT, TO-220",
"LM7805DT, SOT-223",
"LM7805KC, TO-252",
"LM7805T, TO-220"
})
void shouldExtractVoltageRegulatorPackages(String mpn, String expectedPackage) {
assertEquals(expectedPackage, handler.extractPackageCode(mpn));
}
// MPN + multiple expected values
@ParameterizedTest
@CsvSource({
"LM7805, VOLTAGE_REGULATOR_LINEAR_TI",
"LM7812, VOLTAGE_REGULATOR_LINEAR_TI",
"UA7805, VOLTAGE_REGULATOR_LINEAR_TI"
})
void shouldDetect78xxRegulators(String mpn, String expectedType) {
ComponentType type = ComponentType.valueOf(expectedType);
assertTrue(handler.matches(mpn, type, registry));
}
When refactoring an existing handler, follow this 9-step checklist (established in PR #77):
Before:
Set<ComponentType> types = new HashSet<>();
types.add(ComponentType.MOSFET);
return types;
After:
return Set.of(ComponentType.MOSFET, ComponentType.MOSFET_INFINEON);
Before:
private static final Map<String, String> PACKAGE_CODES = Map.of(
"CT", "TO-220",
"DT", "SOT-223"
);
After:
// Use centralized PackageCodeRegistry.resolve("CT") → "TO-220"
// Add new codes to PackageCodeRegistry if missing
ComponentType enum duplicates (use main ComponentType enum)// ✅ CORRECT: Check "DT" before "T"
if (upperMpn.endsWith("DT")) return "SOT-223";
if (upperMpn.endsWith("CT")) return "TO-220";
if (upperMpn.endsWith("T")) return "TO-220";
// ❌ WRONG: "T" matches first, never checks "DT" or "CT"
if (upperMpn.endsWith("T")) return "TO-220";
if (upperMpn.endsWith("DT")) return "SOT-223"; // Never reached!
// ✅ CORRECT
return patterns.matchesForCurrentHandler(upperMpn, type);
// ❌ WRONG: Searches all handlers' patterns
return patterns.matches(upperMpn, type);
registry.addPattern(ComponentType.MOSFET, "^IRF.*");
registry.addPattern(ComponentType.MOSFET_INFINEON, "^IRF.*");
If you register patterns for VOLTAGE_REGULATOR_LINEAR_TI, it must be in getSupportedTypes().
mvn test -Dtest=TIHandlerTest # Single handler test
mvn test -Dtest=*HandlerTest # All handler tests
mvn test # Full suite
| Anti-Pattern | Problem | Solution |
|-------------|---------|----------|
| Using HashSet instead of Set.of() | Mutable set, less efficient | Use Set.of(...) for immutable sets |
| Only registering manufacturer-specific types | Users searching for base type get 0 results | Register BOTH base and specific types |
| Using patterns.matches() in matches() | Cross-handler false matches | Use patterns.matchesForCurrentHandler() |
| Suffix ordering: short before long | "T" matches before "DT", never checks "DT" | Check longer suffixes first (DT → CT → T) |
| Not uppercasing MPN before charAt() | Fails on lowercase MPNs | Always String upperMpn = mpn.toUpperCase() |
| Test in manufacturers package | Classpath shadowing breaks handler loading | Put tests in handlers package, not manufacturers |
| Complex regex when simple startsWith() works | Slower, harder to maintain | Use startsWith() for simple prefix checks |
AVXHandler: Ceramic capacitor manufacturer, simple prefix patterns.
^08[0-9]{4}.* (0805 ceramic), ^12[0-9]{4}.* (1206 ceramic)TEHandler: TE Connectivity connectors.
TIHandler: Texas Instruments (628 lines, but well-structured).
STHandler: STMicroelectronics (240 lines).
NXPHandler: NXP Semiconductors (393 lines).
matchesForCurrentHandler() correctlyInfineonHandler: Infineon Technologies.
MicrochipHandler: Microchip Technology.
ManufacturerHandlerFactory uses TreeSet with deterministic ordering. First matching handler wins in getManufacturerHandler().
All manufacturer-specific types MUST be in the switch statement. Missing types fall through to default -> this (returns self, not base type).
MPNUtils.getComponentType() uses specificity scoring:
Without scoring, HashSet iteration could return IC instead of OPAMP_TI.
PackageCodeRegistryextractSuffixAfterHyphen(), findFirstDigitIndex(), etc.isOfficialReplacement() implementation/component - Base skill for general component work/architecture - Refactoring and cleanup guidance/mpn-normalization - MPN suffix handling and normalizationAbstractManufacturerHandler.java - Base class with helper methodsTIHandlerTest.java, STHandlerTest.java, NXPHandlerTest.java - Test templatesdata-ai
Cost-effective task delegation strategy using Haiku model for straightforward work. Use when planning how to approach simple, pattern-following tasks to minimize costs.
tools
Use when working with component similarity calculations - comparing MPNs, finding equivalent parts, implementing new similarity calculators, or understanding how component matching works.
testing
Use when working with transistor similarity calculations - comparing BJT MPNs, understanding NPN/PNP polarity matching, equivalent groups like 2N2222/PN2222, or transistor-specific similarity logic.
testing
Use when working with sensor similarity calculations - comparing temperature/accelerometer/humidity sensor MPNs, understanding sensor families, equivalent parts, or sensor-specific similarity logic.