diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f5a768..e868658 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,14 @@ For more details or to discuss releases, please visit the ## [Unreleased] +## [0.9.5] - 2026-09-03 + +- KiCad HTTP API: `categoryPrefixedNames: true` in `gitplm.yml` serves parts as + `res/RES-0000-1002` rather than `RES-0000-1002`, matching the names a KiCad + database library used. A project moving from a `.kicad_dbl` library to the + HTTP library can now keep the symbols it has already placed, which previously + had to be found again by hand or rewritten in every schematic. + ## [0.9.4] - 2026-07-16 - Parts whose variation codes a value rather than a plain number, such as diff --git a/README.md b/README.md index 8429501..2a85a4f 100644 --- a/README.md +++ b/README.md @@ -392,6 +392,27 @@ The [`gitplm.yml`](https://github.com/git-plm/parts/blob/main/gitplm.yml) in the example: a `default` section covering most categories, with resistors, capacitors, inductors, and the rest displaying their parametric columns instead. +### Migrating from a KiCad database library + +KiCad names a placed symbol after the `name` the API serves, and writes that +name into the schematic as the symbol's `lib_id`. GitPLM serves the IPN, so a +resistor is saved as `#gplm:RES-0000-1002`. + +A KiCad database library named the same part `#gplm:res/RES-0000-1002`, after +the table it came from. A project moving from `.kicad_dbl` to the HTTP library +therefore finds `*** symbol not found ***` on every part it had already placed. +Setting `categoryPrefixedNames` serves the names in that form instead, so the +existing schematics keep resolving: + +```yaml +http: + categoryPrefixedNames: true +``` + +The alternative is to rewrite the `lib_id` of every symbol in every schematic +that uses the library. New projects can leave the setting off and use the +shorter names. + ### Configuring KiCad KiCad finds the server through a `.kicad_httplib` file that points at it. There diff --git a/config.go b/config.go index 444b806..c0bc656 100644 --- a/config.go +++ b/config.go @@ -27,6 +27,12 @@ type HTTPConfig struct { Enabled bool `yaml:"enabled"` Port int `yaml:"port"` Token string `yaml:"token"` + // CategoryPrefixedNames serves each part under the name + // / rather than the IPN alone, so KiCad writes + // lib_ids like `#gplm:res/RES-0000-1002`. This is the form a KiCad + // database library wrote, so a project moving from `.kicad_dbl` to the + // HTTP library keeps resolving its existing symbols. + CategoryPrefixedNames bool `yaml:"categoryPrefixedNames"` // Fields configures the fields served for each IPN category (RES, CAP, // ...). The "default" key applies to every category, and a category's own // settings are applied on top of it. diff --git a/kicad_api.go b/kicad_api.go index 469a747..4c55fc6 100644 --- a/kicad_api.go +++ b/kicad_api.go @@ -247,6 +247,17 @@ func (s *KiCadServer) extractCategory(ipnStr string) string { return c } +// partName returns the name a part is served under, which is the name KiCad +// writes into a schematic's lib_id. It is the IPN, or / when +// categoryPrefixedNames is set, matching the lib_ids a KiCad database library +// wrote. +func (s *KiCadServer) partName(category, partID string) string { + if !s.httpConfig.CategoryPrefixedNames || category == "" { + return partID + } + return strings.ToLower(category) + "/" + partID +} + // getCategoryDisplayName returns a human-readable name for a category func (s *KiCadServer) getCategoryDisplayName(category string) string { displayNames := map[string]string{ @@ -399,7 +410,7 @@ func (s *KiCadServer) getPartsByCategory(categoryID string) []KiCadPartSummary { // KiCad displays the name as the schematic library link, so use // the IPN rather than the description - partName = partID + partName = s.partName(partCategory, partID) // Get description if descIdx >= 0 && len(row) > descIdx { @@ -506,7 +517,7 @@ func (s *KiCadServer) getPartDetail(partID string) *KiCadPartDetail { // KiCad displays the name as the schematic library link, so use // the IPN rather than the description - partName := partID + partName := s.partName(category, partID) symbolID := values["Symbol"] fields := s.buildFields(category, file.Headers, values) diff --git a/kicad_api_test.go b/kicad_api_test.go index 4b81ba7..f7e84e5 100644 --- a/kicad_api_test.go +++ b/kicad_api_test.go @@ -40,3 +40,44 @@ func TestExtractCategory(t *testing.T) { } } } + +type partNameTest struct { + category string + partID string + name string +} + +func TestPartName(t *testing.T) { + tests := []partNameTest{ + {"RES", "RES-0000-1002", "RES-0000-1002"}, + {"", "RES-0000-1002", "RES-0000-1002"}, + } + + s := &KiCadServer{} + + for _, test := range tests { + got := s.partName(test.category, test.partID) + if got != test.name { + t.Errorf("partName(%q, %q) = %q, want %q", test.category, test.partID, got, test.name) + } + } +} + +func TestPartNameCategoryPrefixed(t *testing.T) { + tests := []partNameTest{ + {"RES", "RES-0000-1002", "res/RES-0000-1002"}, + {"ASY", "ASY-0001-0001", "asy/ASY-0001-0001"}, + // A part whose IPN has no category is served under its IPN alone, + // since there is no prefix to add + {"", "not-an-ipn", "not-an-ipn"}, + } + + s := &KiCadServer{httpConfig: HTTPConfig{CategoryPrefixedNames: true}} + + for _, test := range tests { + got := s.partName(test.category, test.partID) + if got != test.name { + t.Errorf("partName(%q, %q) = %q, want %q", test.category, test.partID, got, test.name) + } + } +} diff --git a/plans/2026-08-31-check-command.md b/plans/2026-08-31-check-command.md new file mode 100644 index 0000000..a3a5296 --- /dev/null +++ b/plans/2026-08-31-check-command.md @@ -0,0 +1,209 @@ +# Plan: `gitplm check` + +- **Date:** 2026-08-31 +- **Status:** Draft +- **Goal:** Move the general half of the parts library's `check-csv.py` into + gitplm as a `check` subcommand, driven by `gitplm.yml` so it is useful to any + library rather than to one. + +## Context + +The [parts](https://github.com/git-plm/parts) library carries a 515-line Python +validator at `.claude/skills/adding-parts/scripts/check-csv.py`. It checks the +same CSV files gitplm serves, and it duplicates work gitplm already does: the +IPN format, raw CSV loading, and the KiCad symbol and footprint references that +`gitplm http` hands to KiCad. It runs in one repository, by hand, and only when +someone remembers to invoke it. + +Two definitions of the IPN format is the clearest cost. `ipn.go` accepts `047n` +and documents why case matters; the Python requires an uppercase variation code +and reports four real parts as malformed: + +``` +$ check-csv.py database/g-ind.csv +database/g-ind.csv: line 8: IPN is not CCC-NNNN-VVVV in capitals: IND-0005-047n +``` + +`IND-0005-047n`, `RES-0002-010m`, `RES-0002-015m` and `RES-0008-8R3m` are all +correct under `partnumbers.md`. The `--new-only` flag hides the reports because +they predate the checker, so the disagreement has gone unnoticed rather than +been resolved. + +`roadmap.md` already asks for one of these checks: "Show error if symbol or +footprint cannot be found." gitplm serves `symbolIdStr` at `kicad_api.go:527` +with no way to know the reference resolves. + +## Scope + +The Python checks fall into three layers, and only the first belongs in Go +unconditionally. + +### Layer 1 -- structural, always on + +These need no knowledge of a particular library's conventions. + +- every row has the header's column count +- the IPN parses, and its category matches the file it is in +- rows are sorted by IPN +- no duplicate IPN +- no comma inside a field, no field padded with whitespace +- `Datasheet` is an `https` URL when the row names an MPN +- a `Replaced by ` status names a part that exists and is not itself + retired +- `Symbol` and `Footprint` resolve against the KiCad libraries on disk +- across files: no MPN carried by two parts that are both still live + +### Layer 2 -- conventions, configured + +The rule is general; the columns it applies to are not. Rather than police a +list that would go stale, the check reports values in one column that differ +only in case, spacing or punctuation from another value in the same column. +Which columns to watch comes from config. + +- `Manufacturer` and `Material` compared loosely, ignoring punctuation +- spec columns (`Voltage`, `Resistance`, `Tolerance`, ...) compared strictly + enough to keep `1.02` and `102` apart +- `C0G` spelled with a letter O, which no other check can catch because the two + are identical in most fonts + +### Layer 3 -- value encodings, configured + +A variation code that encodes a value can be checked against the column holding +that value, and this is the layer that finds genuinely wrong parts rather than +formatting drift. The concept is gitplm's -- `ipn.go` already documents that +`02V5` means 2.5 V -- but the mapping is policy, so ship named encoders and let +a category select one. + +| Encoder | Form | Example | +| ---------- | ----------------------------------- | --------------------- | +| `eia-4dig` | three significant digits plus zeros | `1002` = 10K | +| `ohms-r` | `R` as the decimal point | `97R6`, `0R10` | +| `ohms-m` | trailing `m` for milliohms | `010m`, `8R3m` | +| `cap-mme` | mantissa and exponent in pF | `0104` = 100nF | +| `cap-f` | `F` terminator or decimal point | `220F`, `01F5` = 1.5F | + +The resistance encoders compose: one column, several accepted forms. + +## Non-goals + +- Encoding this library's category policy in gitplm. Which `NNNN` series a part + belongs to, and whether a new series is warranted, stays in the parts repo. +- Replacing the judgment in the `adding-parts` skill. A checker confirms the row + is well formed; it cannot confirm the specs came from the datasheet. + +## CLI surface + +``` +gitplm check [-pmDir ] [-new-only] [file ...] +``` + +With no file arguments, check every CSV in `pmDir`. Report one defect per line +in `path: IPN: message` form, print a per-file summary, and exit non-zero if +anything was reported. + +`-new-only` reads each file as it stands at `git HEAD`, runs the same checks +against it, and reports only defects absent from that baseline. Libraries +acquire linters after they acquire parts, so without it a new defect is +indistinguishable from the hundred that came before. If the tree is not a Git +repository, say so and check everything. + +## Configuration + +Everything opinionated is off unless configured, so a library with different +conventions gets structural checks and nothing else. + +```yaml +check: + symbolDirs: + - symbols + - /usr/share/kicad/symbols + footprintDirs: + - footprints + - /usr/share/kicad/footprints + # Columns whose values should be spelled one way. + nameColumns: [Manufacturer, Material] + specColumns: [Voltage, Current, Power, Tolerance, Resistance, Capacitance] + # Variation code checked against the column that states the value. + encodings: + RES: { column: Resistance, encoders: [eia-4dig, ohms-r, ohms-m] } + CAP: { column: Capacitance, encoders: [cap-mme, cap-f] } +``` + +Library directories default to the KiCad standard paths, honoring +`KICAD_SYMBOL_DIR` and `KICAD_FOOTPRINT_DIR` as the Python does, with any +relative path resolved against the config file's directory. + +## Implementation phases + +**Phase 1 -- structural checks.** New `check.go` with a `checker` type over +`CSVFileCollection`, the `check` subcommand in `main.go`, and the layer 1 checks +that need only the CSV itself. Table-driven tests over small fixtures in +`testdata/`. IPNs parse through `ipn.parse()`; no second regular expression. + +**Phase 2 -- symbol and footprint resolution.** Search the configured +directories for `.kicad_sym` and `.pretty/.kicad_mod`, matching +the symbol name inside the library file. Handle the alternates form, where a +field lists several footprints separated by `;` and the entries after the first +are bare names in the first entry's library. This closes the roadmap item. + +**Phase 3 -- convention checks.** Grouping by a loose key for names and a strict +one for measured values, with both column lists from config. + +**Phase 4 -- value encodings.** The encoder table above, plus the parsers for +the value columns they compare against, as a small interface so a library can be +given a new form without touching the checks around it. + +**Phase 5 -- cross-file checks.** Manufacturer spelling and duplicate live MPNs +across every file, which a single file cannot see. + +**Phase 6 -- wire into the rest of gitplm.** Run the structural checks when +`gitplm http` loads or reloads the database and log what fails, so a reference +KiCad cannot resolve is visible at the server rather than in KiCad. Surface the +same results in the TUI. + +**Phase 7 -- retire the Python.** Covered below. + +## Technical decisions + +- **One definition of the IPN.** `reIpn` in `ipn.go` stays the only one. + Reconciling with the Python means the four lowercase variation codes stop + being errors, which is correct: `partnumbers.md` specifies that case carries + the SI prefix. +- **`-new-only` shells out to `git show HEAD:`.** No Git library + dependency for one read, and it degrades to checking everything when the + command fails. +- **Quiet by default for other libraries.** Layers 2 and 3 report nothing unless + `check:` names columns and encodings, so adopting a new gitplm version never + turns someone's working library red. +- **Defects are values, not printed text.** A `defect` struct with file, line, + IPN and message keeps the baseline comparison in `-new-only` reliable and + leaves room for a `-json` flag later. + +## Parity checklist + +Track against the Python so the retirement is a decision rather than a guess. + +| Check | Layer | Status | +| ---------------------------------- | ----- | ------ | +| column count | 1 | | +| IPN format and category match | 1 | | +| sort order | 1 | | +| duplicate IPN | 1 | | +| comma in field | 1 | | +| padded field | 1 | | +| datasheet URL | 1 | | +| `Replaced by` target resolves | 1 | | +| symbol and footprint resolve | 1 | | +| duplicate live MPN across files | 1 | | +| manufacturer spelling across files | 1 | | +| spelling drift within a file | 2 | | +| C0G with a letter O | 2 | | +| resistance code matches column | 3 | | +| capacitance code matches column | 3 | | + +## Retiring `check-csv.py` + +Keep both until `gitplm check -new-only database/g-*.csv` reproduces the +Python's output on the parts library. Then delete the script, point the +`adding-parts` skill and the parts `CLAUDE.md` commands section at the binary, +and note the new command in `CHANGELOG.md` under `[Unreleased]`. diff --git a/plans/plans.md b/plans/plans.md index 7ec0172..41e581b 100644 --- a/plans/plans.md +++ b/plans/plans.md @@ -4,3 +4,4 @@ | -------------------------------------- | --------------- | ------------- | | 2026-02-27-tui-interactive-features.md | **Implemented** | ef015b5 | | 2026-03-02-detail-popup-datasheet.md | **Implemented** | 338e9ea | +| 2026-08-31-check-command.md | Draft | efe5a1a |