diff --git a/SKILL.md b/SKILL.md index e96c62d..4bf727e 100644 --- a/SKILL.md +++ b/SKILL.md @@ -55,6 +55,27 @@ and report it. A symbol that still holds Ultra-Librarian defaults is an incomple Every other flow keeps the normal interactive asks below. +## Keep a copy in the connected folder — always, after every part push + +So the operator ends up with the parts they worked on **on their own machine** — without +having to go into the admin Gitea to fetch them — the skill drops a copy of each pushed part +folder into the **connected session folder**. At the start of a Cowork session the operator +connects a folder; that folder is the destination. If no folder was explicitly connected, use +the session's output directory `/mnt/user-data/outputs/` (create it if missing) so the copy +still lands somewhere the operator can retrieve. + +The rule is: **immediately after any successful part push to Gitea**, copy that part's +`__/` folder — with its files intact (`.xlsx`, datasheet, `.SchLib`, +`.PcbLib`) — into the connected folder. This is automatic and never a question, exactly like +the push itself. Details and the exact command are in *Copy the pushed folder to the connected +folder* below. + +Two boundaries: (1) copy **only after the push succeeds** — a failed push must not leave a +misleading copy on the operator's machine; (2) this applies to **part** pushes only +(`push-part`, and the `commit-push` used by updates, backfill, and the `.SchLib` fill-and-push). +**Skill-repo** pushes (`push-skill`, template/changelog changes) involve no part folder and are +excluded. + ## Inputs - **A datasheet PDF whose filename is the MPN** (e.g. `BAT46WJ.pdf`). If it's a series @@ -256,7 +277,9 @@ python scripts/fill_templates.py part.json \ This writes `.xlsx` with column A = the tag, **Skill Version (col B)** and **Template Version (col C)** stamped from this typeid's current versions, and the four design columns -left blank for now. +left blank for now. Every sheet also carries an **`MPN` column** (positioned just before +`Manufacturer`) that `fill_templates` populates with the part's bare MPN — it's filled from +`part["mpn"]` automatically, so no extra input is needed. The part workbook has up to **two sheets**: @@ -318,11 +341,9 @@ new-part run. Do not ask "would you like me to fill the symbol parameters?", do a suggestion, and do not defer it to a later turn. Just do it, then report that it's done. The SOP requires the fixed parameter set (Manufacturer, Manufacturer Part, Value, Tolerance, -Operating Temperature, RoHS, Datasheet, Process, Vecmocon Part Code, …) on every symbol, **plus -the symbol's Description**, so a part folder whose `.SchLib` has not been filled is **incomplete -and must not be pushed**. Build the part's `params.json` from the values the engineer just -verified in step 5 — including `"description"`, set to the **same** strict `Class_TYPEID` string -you put in the part's Excel — and run: +Operating Temperature, RoHS, Datasheet, Process, Vecmocon Part Code, …) on every symbol, so a +part folder whose `.SchLib` has not been filled is **incomplete and must not be pushed**. Build +the part's `params.json` from the values the engineer just verified in step 5 and run: ```bash python scripts/schlib_write.py \ @@ -331,6 +352,12 @@ python scripts/schlib_write.py \ --out //.SchLib ``` +`params.json` must carry **`description`** and **`comment`** alongside `parameters`. The +`description` is the strict `Class_TYPEID_…` string you already built for the workbook — reuse +the *exact same string* so the symbol's ComponentDescription and the Excel `Description` column +never diverge. The `comment` is the MPN. If the script warns that `description` is missing, treat +that as an error: fix `params.json` and re-run before pushing. + See *Mandatory symbol parameters* below for the full parameter set, the value-shorthand rules, and the Ultra-Librarian default-stripping behaviour — read `references/schlib_parameters.md` before building the parameter set. The values are already verified at this point, so this step @@ -354,22 +381,38 @@ python scripts/gitea_components.py push-part --folder / --typeid //` — creating the Class folder if it doesn't exist yet, or pushing into it if it does — and commits and pushes. Confirm to the -user where it landed. +user where it landed. **Then copy the folder to the connected folder** (next section). + +### Copy the pushed folder to the connected folder — automatic, after every part push + +Right after the push above **succeeds**, copy the just-pushed `__/` folder +into the connected session folder so the operator has a local copy without touching Gitea. Use +the folder the operator connected at the start of the session; if none was connected, fall back +to `/mnt/user-data/outputs/`. + +```bash +DEST="${CONNECTED_FOLDER:-/mnt/user-data/outputs}" # the folder connected this session +mkdir -p "$DEST/" +cp -r //. "$DEST//" # xlsx, datasheet, .SchLib, .PcbLib +``` + +Copy the folder as `/` (i.e. `__/`) — just the part folder, not the +`/` parent — so the connected folder simply accumulates one folder per part the operator +worked on. If that folder already exists there (a re-run or an update), refresh it so the local +copy matches what was just pushed. Then tell the user the copy is in the connected folder. + +This copy step runs after **every** part push, including the `commit-push` used by *Updating an +existing part*, *Backfilling existing parts*, and the `.SchLib` *fill-and-push* — run the same +three lines against that part's folder once its push succeeds. It does **not** run for +`push-skill` (skill-repo/template/changelog pushes carry no part folder). ## Mandatory symbol parameters (.SchLib) The SOP (§5) requires every schematic symbol to carry a fixed parameter set in its Altium properties — `Component Type`, `Manufacturer`, `Manufacturer Part`, `Value`, `Tolerance`, `Operating Temperature`, `ROHS`, `Datasheet`, `Process`, `Vecmocon Part Code`, and the two -second-source fields — with the **Comment** set to the MPN and the **Description** set to the -part's strict `Class_TYPEID` string. The skill stamps all of these onto the symbol from the -datasheet in a single pass. - -**Description is written too, always.** It isn't a parameter — it's the `ComponentDescription` -field in the symbol's header, which Ultra-Librarian ships as the placeholder text `Description`. -So it must be written explicitly or the symbol shows the literal word "Description" in Altium -even when every parameter is right. Pass `"description"` in `params.json` (it falls back to -`parameters.Description`), and use the **same** string as the part's Excel so the two agree. +second-source fields — with the **Comment** set to the MPN. The skill can stamp these onto the +symbol from the datasheet. This runs **as its own task too**, not only inside new-part creation: whenever the user hands over one or more `.SchLib` files and wants their parameters filled/updated (e.g. "\schlib", "add @@ -388,7 +431,12 @@ UL `Component_Type` — replaced by Vecmocon's own spaced `Component Type`). Kin - **Read from the datasheet** (don't just echo given text — open the PDF and fill the real, verified values): `Value` = the **value only** in shorthand (e.g. `1u`, `12p`, `10k` — no package), plus `Manufacturer`, `Manufacturer Part`, `Operating Temperature`, `Tolerance`, - `ROHS`, `Datasheet`, and `Process` by inference from the package. + `ROHS`, and `Process` by inference from the package. +- **Derived link** — `Datasheet` = the **Gitea URL to the pushed `_data.`**, so the + symbol clicks straight through to the exact datasheet in the library. Build it from `GIT_HOST` + + `LIBRARY_REPO` and the part's Class/tag + (`https:////raw/branch/main///_data.`); the full + rule is in `references/schlib_parameters.md`. - **Leave blank for now** — `Manufacturer 2` / `Manufacturer Part 2` (the second source). Don't populate these by default; they stay hidden in Altium until filled later. (An optional cross-reference search to find a second source is documented in @@ -402,8 +450,7 @@ Leave any genuinely-unknown field blank — the SOP hides blank parameters, so a empty until filled. The full method for the second-source search is in `references/schlib_parameters.md`. -Collect the values into a `params.json` — with `"description"` alongside `"parameters"` — and -write them into the symbol: +Collect the values into a `params.json` and write them into the symbol: ```bash python scripts/schlib_write.py --schlib .SchLib --params params.json --out //.SchLib @@ -464,7 +511,8 @@ the symbol clearly isn't this component, stop and flag it rather than pushing th Pushing without asking is consistent with the skill's standing rule that pushing is automatic; what's new here is that the Excel fill is automatic too. Tell the user which parameters you filled, that the symbol was updated, and where in Gitea it landed. (A part-data fill isn't a - template change, so nothing version-bumps and the changelog is untouched.) + template change, so nothing version-bumps and the changelog is untouched.) **Then copy the + part folder to the connected folder** — see *Copy the pushed folder to the connected folder*. ## Per-typeid versioning @@ -477,6 +525,14 @@ into that typeid's rows (cols B and C), so a row always records the template/ski was built against. `append_parameter.py` does the bump; `common.py` is the single source for reading and writing these numbers. +**One deliberate exception already applied:** the `MPN` column (before `Manufacturer`) was +added to **every** typeid sheet as a one-off structural change **without** bumping any +`template_version`/`skill_version` and **without** a changelog row — it's a bookkeeping column, +not a spec parameter, and versions were intentionally left where they were. Don't treat the +existing sheets' MPN column as a version discrepancy to "correct." (This no-bump behaviour is +specific to that one change; genuine parameter additions still go through `append_parameter.py` +and bump normally.) + ## The changelog `append_parameter.py` maintains one **global** changelog as an Excel workbook at @@ -519,6 +575,9 @@ and tell the user the previous sheets were updated: python scripts/gitea_components.py commit-push --root work/ --message "backfill into " ``` +Then copy each rebuilt part folder into the connected folder (see *Copy the pushed folder to +the connected folder*) so the operator's local copies reflect the backfill too. + ## Updating an existing part When the duplicate check finds the part (or the user asks to revise one that's already in diff --git a/assets/CHANGELOG.xlsx b/assets/CHANGELOG.xlsx index 9a87278..6985af4 100644 Binary files a/assets/CHANGELOG.xlsx and b/assets/CHANGELOG.xlsx differ diff --git a/assets/template/template.xlsx b/assets/template/template.xlsx index 8292a39..4335134 100644 Binary files a/assets/template/template.xlsx and b/assets/template/template.xlsx differ diff --git a/references/schlib_parameters.md b/references/schlib_parameters.md index d94d291..cb77889 100644 --- a/references/schlib_parameters.md +++ b/references/schlib_parameters.md @@ -22,16 +22,9 @@ inside Altium; keep it as a fallback if a particular file doesn't round-trip.) Use these exact Altium parameter names (they must match the symbol, per the SOP screenshot). The **Comment** field is set to the MPN (SOP §4), and the **Description** field is the strict -string from `references/description_format.md`. - -**The Description is not a parameter record.** It is the `ComponentDescription` field of the -component's `RECORD=1` header — the box shown at *Properties → General → Description*, above the -Parameters table. Ultra-Librarian ships it as the literal placeholder text `Description`, so a -symbol whose parameters are all correctly filled will *still* show the word "Description" in -Altium unless that header field is rewritten. `schlib_write.py` now sets it in the same pass as -the parameters: pass `"description"` in `params.json`, or let it fall back to your -`parameters.Description`. Always give it the same strict `Class_TYPEID` string you wrote into the -part's Excel, so the symbol and the workbook agree. +string from `references/description_format.md` — the *same* string as the part's Excel +`Description` column. Both are written by `schlib_write.py` from the `comment` and `description` +keys of `params.json`; neither is optional. | Parameter | Source | Notes | |-----------|--------|-------| @@ -45,16 +38,33 @@ part's Excel, so the symbol and the workbook agree. | `Vecmocon Part Code` | engineer | internal code (e.g. `VECESC2421`) — **not on the datasheet**; ask | | `Operating Temperature` | datasheet | full range, e.g. `-55 °C to +125 °C` | | `Tolerance` | datasheet | e.g. `1%` (or `±1%`) | -| `Datasheet` | datasheet | source URL or document reference (SOP marks this optional) | +| `Datasheet` | derived | **Gitea link to the pushed `_data.`** — see below (SOP marks this optional) | | `ROHS` | datasheet | RoHS compliance, `Yes`/`No` | **Read the datasheet-sourced values from the actual datasheet — don't echo whatever text was handed to you.** Open the PDF, find each real value (`Value`, `Manufacturer Part`, -`Manufacturer`, `Operating Temperature`, `Tolerance`, `Datasheet`, `ROHS`, and `Process` by +`Manufacturer`, `Operating Temperature`, `Tolerance`, `ROHS`, and `Process` by inference from the package), and fill them verified. An honest blank beats a guess — the SOP hides blank parameters, so a gap just stays empty until someone fills it. Only **one** field is purely internal and must come from the engineer: `Vecmocon Part Code` — ask for it. +**The `Datasheet` field is a link, not a value read off the PDF.** Point it at the datasheet +this skill pushes into the part's Gitea folder (`_data.`), so opening the symbol in +Altium clicks straight through to the exact datasheet used. Build it from the config +(`GIT_HOST`, `LIBRARY_REPO` in `config/gitea.env`) and the part's Class + tag: + +``` +https:////raw/branch/main//__/_data. +``` + +e.g. `https://gitea.vecmocon.com/nitishKumar/library/raw/branch/main/Diode/BAT46WJ_Nexperia_SCH/BAT46WJ_data.pdf`. +Use the `raw/branch/` form (not `src/…`) so the URL opens the PDF directly; +`` is `class_folder(typeid)` and `__` is the same tag the folder is +pushed under, so the link always resolves to the file that lands in Gitea. Fill this link even +on the first push of a new part — the push and the symbol write happen in the same run, so the +URL is valid as soon as the part folder lands. (If for some reason the datasheet isn't being +pushed to Gitea, leave `Datasheet` blank rather than writing a link that won't resolve.) + The second-source pair (`Manufacturer 2` / `Manufacturer Part 2`) is **left blank for now** — don't populate it by default. It simply stays hidden in Altium until someone fills it later. @@ -85,11 +95,22 @@ confirm** with the specs you compared. Collect the values into a `params.json` (same spirit as `part.json`). `component` is the symbol's Library Ref (from `altium_refs.py`); omit it to apply to every component in the lib. +Two top-level fields sit alongside `parameters` and are **both mandatory**: + +- **`description`** → the symbol's `ComponentDescription`. This must be **byte-identical to the + part's Excel `Description` column** — the same strict `Class_TYPEID_…` string built to + `references/description_format.md`. Build it once, use it in both places; never let the two + drift. A symbol must never ship carrying Altium's `No Description Available` placeholder. +- **`comment`** → the Comment record's Text, which per SOP §4 is always the exact MPN. + +Both are patched **in place**, so the records keep their other fields (Comment keeps its +Location/Color/FontID); only the parameter block is dropped and re-added. + ```json { "component": "CC0402FRNPO9BN120", + "description": "Capacitor_CER_12pF_50V_±1%_0402_c0g", "comment": "CC0402FRNPO9BN120", - "description": "Capacitor_CER_12pF_50V_±1%_0402_NPO", "parameters": { "Component Type": "Capacitor", "Value": "12pF_0402", @@ -101,7 +122,7 @@ symbol's Library Ref (from `altium_refs.py`); omit it to apply to every componen "Vecmocon Part Code": "VECESC2421", "Operating Temperature": "-55 °C to +125 °C", "Tolerance": "1%", - "Datasheet": "https://www.lcsc.com/datasheet/C326662.pdf", + "Datasheet": "https://gitea.vecmocon.com/nitishKumar/library/raw/branch/main/Capacitor/CC0402FRNPO9BN120_YAGEO_CER/CC0402FRNPO9BN120_data.pdf", "ROHS": "Yes" } } @@ -117,9 +138,16 @@ python scripts/schlib_write.py --schlib .SchLib --params params.json --out < `params.json` may carry a `"remove"` list (defaults to `["Manufacturer_Name", "Manufacturer_Part_Number", "Copyright", "Component_Type"]`); those Ultra-Librarian defaults are -stripped and the SOP params — including the spaced `Component Type` = Class — added. The script targets the component named in `"component"` (its Library Ref / storage name), -or every component if omitted, and self-checks the output re-opens as a valid OLE. Deliver the +stripped and the SOP params — including the spaced `Component Type` = Class — added. The script +also sets `ComponentDescription` from `"description"` and the Comment Text from `"comment"`, and +warns on stderr if `"description"` is missing — **treat that warning as an error** and re-run +with the Description rather than shipping the symbol. It targets the component named in `"component"` (its Library Ref / storage name), +or every component if omitted, and self-checks the output re-opens as a valid OLE with the +params and Description present. Deliver the resulting `.SchLib`, and have the engineer open it in Altium once to confirm it loads, then Save to Server with a revision note per the SOP. +The script needs the `olefile` package (`pip install olefile`) — its only non-stdlib +dependency. Install it if the import fails. + Scope note: the d \ No newline at end of file diff --git a/scripts/fill_templates.py b/scripts/fill_templates.py index c17973b..ed1ba49 100644 --- a/scripts/fill_templates.py +++ b/scripts/fill_templates.py @@ -95,6 +95,7 @@ def part_to_row(part, headers, design=None): if key == norm(COL_TAG): row[h] = tag elif key == norm(COL_SKILL_VER): row[h] = sver elif key == norm(COL_TEMPLATE_VER): row[h] = tver + elif key == norm("MPN"): row[h] = part.get("mpn", vals.get("mpn", "")) elif key == norm("Manufacturer"): row[h] = part.get("manufacturer", vals.get("manufacturer", "")) elif h in DESIGN_COLS: row[h] = dmap.get(key, vals.get(key, "")) else: row[h] = vals.get(key, "") diff --git a/scripts/schlib_write.py b/scripts/schlib_write.py index 1c58728..48ebd7d 100644 --- a/scripts/schlib_write.py +++ b/scripts/schlib_write.py @@ -16,28 +16,29 @@ What it does to the target component's Data stream: and the UL ``Component_Type`` (Vecmocon adds its own spaced ``Component Type`` = Class instead); override this default set with a "remove" list in params.json, - adds/updates the SOP parameters from params.json (see references/schlib_parameters.md), - - sets the component's ComponentDescription (the Description shown in Altium's Properties - panel) — this lives in the RECORD=1 header, NOT in a parameter record, and Ultra-Librarian - ships it as the literal placeholder "Description", so it must be rewritten explicitly, - - leaves pins, graphics, the Comment and all other records exactly as they were. + - sets the component's ComponentDescription (RECORD=1) from params.json "description" — the + strict Description string from references/description_format.md, the SAME text that goes in + the part's Excel Description column. Mandatory: a symbol must never ship with Altium's + "No Description Available" placeholder, + - sets the Comment record's Text from params.json "comment" (SOP §4: Comment = the MPN), + patching that record in place so its position/colour/font are preserved, + - leaves pins, graphics and all other records exactly as they were. Usage: python schlib_write.py --schlib IN.SchLib --params params.json --out OUT.SchLib params.json (same shape altium_params.py uses): {"component":"JMK105BJ105KV-F", # LibRef / component-storage name; omit -> all comps - "description":"Capacitor_CER_1uF_35V_±10%_0402_x7r", # optional; else parameters.Description + "description":"Capacitor_CER_1uF_10v_±10%_0402_x5r", # -> ComponentDescription; == Excel Description + "comment":"JMK105BJ105KV-F", # -> Comment record Text (the MPN) "parameters":{"Value":"1u","Manufacturer":"Taiyo Yuden","Component Type":"Capacitor", ...}, "remove":["Manufacturer_Name","Manufacturer_Part_Number","Copyright","Component_Type"]} # optional; default -The Description is the same strict Class_TYPEID string written to the part's Excel -(references/description_format.md), so symbol and workbook always agree. - IMPORTANT: this writes Altium's own binary format from outside Altium. It is validated to re-open as a well-formed OLE with every other stream byte-identical, but ALWAYS open the result in Altium once to confirm it loads before relying on it. """ -import argparse, json, struct, sys, hashlib +import argparse, json, re, struct, sys, hashlib import olefile FREESECT=0xFFFFFFFF; ENDOFCHAIN=0xFFFFFFFE; FATSECT=0xFFFFFFFD @@ -108,6 +109,21 @@ def _param_record(idx, name, value): return struct.pack(', + so any edit that changes the payload length must rewrite the prefix.""" + payload=text.encode('latin-1')+b'\x00' + return struct.pack('=... up to the next | (or end of record). Adds the field if absent.""" + pat=re.compile(r'\|'+re.escape(field)+r'=[^|]*') + if pat.search(text): + return pat.sub('|'+field+'='+value, text, count=1) + return text+'|'+field+'='+value + + def _leading_text_records(data): """Split Data into (list of leading text-record byte-blocks, tail bytes). The leading run is the contiguous |RECORD=…| text records at the start (RECORD=1 + the parameter block); it @@ -129,40 +145,15 @@ def _rec_name(block): return t.split('|Name=')[1].split('|')[0] if '|Name=' in t else None -def _text_block(text): - """Re-wrap an edited text record as its 4-byte length prefix + NUL-terminated payload.""" - payload=text.encode('latin-1')+b'\x00' - return struct.pack(' General -> Description) is NOT a parameter - record — it's the ComponentDescription field of the RECORD=1 header. Ultra-Librarian ships - the literal placeholder `ComponentDescription=Description`, so unless this is rewritten the - symbol shows the word "Description" in Altium even after every SOP parameter is filled. - """ - text=blk[4:-1].decode('latin-1') - if not text.startswith('|RECORD=1|'): - return blk - if 'ComponentDescription=' in text: - parts=text.split('|') - for i,tok in enumerate(parts): - if tok.startswith('ComponentDescription='): - parts[i]='ComponentDescription='+description - text='|'.join(parts) - elif '|PartCount=' in text: # field absent entirely -> insert it - text=text.replace('|PartCount=', f'|ComponentDescription={description}|PartCount=', 1) - else: - text=text+f'|ComponentDescription={description}' - return _text_block(text) - - -def edit_data(data, params, remove, description=None): +def edit_data(data, params, remove, description=None, comment=None): """Return a new Data stream: drop `remove` params, drop any SOP-name params (re-added - fresh), keep everything else, then append the SOP params. When `description` is given, the - RECORD=1 ComponentDescription field is rewritten too. Pins/graphics/tail untouched.""" + fresh), keep everything else, then append the SOP params. Pins/graphics/tail untouched. + + `description` (when given) overwrites ComponentDescription on the RECORD=1 header — this is + the strict Description string, the same text as the part's Excel Description column, and is + required on every Vecmocon symbol. `comment` overwrites the Comment record's Text (the MPN). + Both are patched IN PLACE so the records keep their other fields (Comment keeps its + Location/Color/FontID), unlike the parameter block which is dropped and re-added.""" leading, tail = _leading_text_records(data) sop_names=set(params) kept=[] @@ -170,8 +161,11 @@ def edit_data(data, params, remove, description=None): nm=_rec_name(blk) if nm is not None and (nm in remove or nm in sop_names): continue # drop UL duplicates + stale SOP copies - if description is not None and nm is None: - blk=_set_description(blk, description) # RECORD=1 header carries the Description + txt=blk[4:-1].decode('latin-1') + if description is not None and txt.startswith('|RECORD=1|'): + blk=_reframe(_sub_field(txt, 'ComponentDescription', description)) + elif comment is not None and nm=='Comment' and '|RECORD=41|' in txt: + blk=_reframe(_sub_field(txt, 'Text', comment)) kept.append(blk) added=[_param_record(20+i, nm, val) for i,(nm,val) in enumerate(params.items())] return b''.join(kept)+b''.join(added)+tail @@ -259,9 +253,12 @@ def write_params(schlib, params_json, out): component=params_json.get("component") or None fields=params_json.get("parameters", {}) or {} remove=params_json.get("remove", DEFAULT_REMOVE) - # The symbol's Description: taken from "description", else the "Description" parameter if the - # caller put it there (it's the same strict Class_TYPEID string that goes in the part's Excel). - description=params_json.get("description") or fields.get("Description") or None + description=params_json.get("description") or None + comment=params_json.get("comment") or None + if not description: + print("WARNING: no 'description' in params.json — the symbol keeps whatever " + "ComponentDescription it has (often Altium's 'No Description Available'). " + "Vecmocon symbols must carry the Description; add it and re-run.", file=sys.stderr) entries, paths, content = read_container(schlib) # target Data stream sid(s): a stream named 'Data' whose parent storage == component (or all) @@ -273,7 +270,7 @@ def write_params(schlib, params_json, out): if not targets: sys.exit(f"no component Data stream found" + (f" for '{component}'" if component else "")) for sid in targets: - content[sid]=edit_data(content[sid], fields, remove, description) + content[sid]=edit_data(content[sid], fields, remove, description, comment) blob=rebuild(entries, content) open(out,'wb').write(blob) @@ -282,21 +279,24 @@ def write_params(schlib, params_json, out): if not olefile.isOleFile(out): sys.exit("ERROR: rebuilt file is not a valid OLE — aborting") ole=olefile.OleFileIO(out) - ok=True; desc_ok=(description is None) + ok=True for e in ole.listdir(streams=True): if e[-1].lower()=="data": t=ole.openstream(e).read().decode('latin-1','ignore') for nm in fields: if f"|Name={nm}|" not in t and f"|Name={nm}\x00" not in t and f"Name={nm}" not in t: ok=False - if description is not None and f"ComponentDescription={description}|" in t: - desc_ok=True + if description and f"|ComponentDescription={description}|" not in t \ + and not t.split("|ComponentDescription=")[-1].startswith(description): + ok=False ole.close() n_comp=len(targets) - desc_note=f"; description set" if description else "; no description given" - flag='[self-check OK]' if (ok and desc_ok) else '[WARN: verify params/description]' + extra=[] + if description: extra.append(f"description={description!r}") + if comment: extra.append(f"comment={comment!r}") print(f"wrote {out} ({len(fields)} params into {n_comp} component(s); removed {remove}" - f"{desc_note}) {flag}") + + ("; " + ", ".join(extra) if extra else "") + ") " + f"{'[self-check OK]' if ok else '[WARN: verify params]'}") def main():