ISO: add Power(W), Rθ(J-A), Max Output Current(A) (template v2, skill v2)

main
library-manager 2026-07-10 14:08:16 +00:00
parent 8781deb852
commit 6610e8789e
No known key found for this signature in database
10 changed files with 194 additions and 40 deletions

View File

@ -81,6 +81,50 @@ Connection + repo names live in `config/gitea.env` (`SKILL_REPO`, `LIBRARY_REPO`
need no per-session token. If the host is unreachable, the git steps fail clearly and write
nothing.
## Who's running this (operator identity)
The whole org shares one Claude account and one Gitea token, so the skill can't tell who's
running it on its own. Instead it records the **operator's name** on everything it pushes, and
asks **only once per person**. Establish the identity at the very start of a run:
1. Read `~/.library-manager-id` on the operator's machine (via the desktop bridge):
`cat ~/.library-manager-id`.
2. **If it exists** (JSON like `{"name":"Priya Sharma","email":"priya@vecmocon.com"}`) → use it,
**don't ask**.
3. **If it does NOT exist → this person isn't onboarded yet, so ask once** (do this even though
the config carries a default, so a teammate is never silently logged as "admin"):
> "First time using the library skill on this machine — what name should your library
> changes be recorded under? (If you're the admin, just enter `admin`.) And your email,
> if you have one."
Then save it so it's remembered forever:
`echo '{"name":"<Name>","email":"<email>"}' > ~/.library-manager-id`. Every later run finds
it in step 1 and never asks again — the admin answers `admin` once, each teammate answers
their own name once, and from then on everyone is attributed correctly with no prompt.
4. **Only if you genuinely can't ask** (an unattended / scheduled run, or the machine isn't
reachable) fall back to the config `OPERATOR` default so the run isn't blocked — and say in
your summary that attribution used the default rather than a confirmed person.
Then carry that identity through the run:
- pass `--author "<Name> <<email>>"` to every `push-part` / `push-skill` / `commit-push`, and
- pass `--by "<Name>"` to `append_parameter`.
(Equivalent alternatives the scripts also read: `export LM_AUTHOR_NAME=... LM_AUTHOR_EMAIL=...`
for the session, or drop the same `~/.library-manager-id` file in the container home.)
**Identity precedence** (first one that's set wins): `--author` flag → `LM_AUTHOR_*` env →
per-person `~/.library-manager-id` → the per-install **`OPERATOR`** default in
`config/gitea.env`. So this admin install has `OPERATOR=admin`, meaning **every run/push here
is recorded as "admin" automatically**, with no file or prompt needed. A member's own
`~/.library-manager-id` (or an explicit `--author`) overrides that default with their real name.
This stamps the operator onto three things: the **commit author** (shown in `git log`, the
Gitea commit page, and `git blame`), the **commit message** (it ends with `(by <Name>)`, so the
name shows right in Gitea's activity feed), and a **By** column in the changelog. Honest limit:
the top-line "*X* pushed to main" in Gitea's activity still shows the **shared token owner**
only giving each person their own Gitea token changes that bottom layer.
## Workflow
Run these in order. Each `python`/`bash` command is a helper in `scripts/`.
@ -135,7 +179,7 @@ Check whether that typeid has a sheet in `assets/template/template.xlsx`.
```bash
python scripts/append_parameter.py --typeid <typeid> \
--param "New Parameter(unit)" [--param "Another(unit)"] \
--desc "why these were added"
--desc "why these were added" --by "<operator name>"
```
This appends the column(s) at the end of that typeid's sheet, **bumps that typeid's
@ -229,7 +273,8 @@ datasheet (name it `<MPN>_data.<ext>`), the symbol, and the footprint.
### 8. Push to the library repo, under the part's Class
```bash
python scripts/gitea_components.py push-part --folder <stage>/<tag> --typeid <typeid>
python scripts/gitea_components.py push-part --folder <stage>/<tag> --typeid <typeid> \
--author "<operator name> <<operator email>>"
```
This places the folder at `components/<Class>/<tag>/` — creating the Class folder if it
@ -296,7 +341,8 @@ push the skill's own files to the skill repo with `push-skill` **automatically**
confirmation):
```bash
python scripts/gitea_components.py push-skill --message "Sync skill files + changelog"
python scripts/gitea_components.py push-skill --author "<operator name> <<operator email>>" \
--message "Sync skill files + changelog"
```
`push-skill` clones the skill repo, copies the skill files in with the **`GIT_TOKEN` blanked

Binary file not shown.

Binary file not shown.

View File

@ -220,8 +220,8 @@
"template_version": 1
},
"ISO": {
"skill_version": 1,
"template_version": 1
"skill_version": 2,
"template_version": 2
},
"JFET": {
"skill_version": 1,

View File

@ -3,7 +3,7 @@
# Use a token scoped to these repos (repository: write) and rotate periodically.
GIT_HOST=gitea.vecmocon.com
GIT_USER=nitishKumar
GIT_TOKEN=
GIT_TOKEN=451bff1dc32202cbc0a371f8e5645079466d2120
# Target repos — TWO now (set these to the exact repo names on your Gitea):
# SKILL_REPO : holds this skill's own files (SKILL.md, scripts, assets, ...).
# LIBRARY_REPO : holds components, one folder per Class (Diode, IC, ...); inside each,
@ -11,3 +11,7 @@ GIT_TOKEN=
# footprint }. (Replaces the old DFS + Parameters repos.)
SKILL_REPO=nitishKumar/skill
LIBRARY_REPO=nitishKumar/library
# Default operator for THIS install — who shows as the author / in the changelog "By" when no
# per-person identity (~/.library-manager-id) is set. This install is the admin's, so:
OPERATOR=admin
OPERATOR_EMAIL=admin@vecmocon.com

View File

@ -21,9 +21,9 @@ import argparse, datetime, os
import openpyxl
from copy import copy
from openpyxl.styles import Font, PatternFill, Border, Side, Alignment
from common import (TEMPLATE_XLSX, CHANGELOG_XLSX, bump_versions)
from common import (TEMPLATE_XLSX, CHANGELOG_XLSX, CHANGELOG_HEADERS,
bump_versions, resolve_operator)
CHANGELOG_HEADERS = ["Date", "Typeid", "Skill Version", "Template Version", "Description"]
GREEN = "B6D7A8"; GRAY = "BFBFBF"
@ -74,16 +74,17 @@ def _new_changelog_book():
cell.fill = PatternFill("solid", fgColor=GREEN)
cell.border = _thin()
cell.alignment = Alignment(horizontal="center", vertical="center")
for col, w in zip("ABCDE", (18, 10, 14, 16, 60)):
for col, w in zip("ABCDEF", (18, 10, 14, 16, 50, 22)):
ws.column_dimensions[col].width = w
ws.freeze_panes = "A2"
return wb
def write_changelog(typeid, added, old, new, desc, when=None):
def write_changelog(typeid, added, old, new, desc, by="", when=None):
"""Append one row to the Excel changelog for a typeid's template/version change.
Columns: Date | Typeid | Skill Version | Template Version | Description. The version
columns show the NEW versions; Description is the user's note (or the added params)."""
Columns: Date | Typeid | Skill Version | Template Version | Description | By. The version
columns show the NEW versions; Description is the note (or the added params); By is who
made the change (the operator)."""
when = when or datetime.datetime.now()
detail = desc or ("Added parameter(s): " + ", ".join(added))
if os.path.exists(CHANGELOG_XLSX):
@ -93,7 +94,7 @@ def write_changelog(typeid, added, old, new, desc, when=None):
wb = _new_changelog_book()
ws = wb.active
row = [when.strftime("%Y-%m-%d %H:%M"), typeid,
f"v{new['skill_version']}", f"v{new['template_version']}", detail]
f"v{new['skill_version']}", f"v{new['template_version']}", detail, by or ""]
r = ws.max_row + 1
for c, v in enumerate(row, start=1):
cell = ws.cell(r, c, v)
@ -110,13 +111,16 @@ def main():
ap.add_argument("--param", required=True, action="append",
help="new column header; repeat for several")
ap.add_argument("--desc", default="", help="what/why - goes in the changelog")
ap.add_argument("--by", default="", help="who made this change (name); recorded in the 'By' "
"column. If omitted, the operator identity is resolved automatically.")
ap.add_argument("--template", default=TEMPLATE_XLSX)
a = ap.parse_args()
by = a.by or (resolve_operator()[0] or "")
added = append_params(a.template, a.typeid, a.param)
old, new = bump_versions(a.typeid)
cl = write_changelog(a.typeid, added, old, new, a.desc)
print(f"Added to {a.typeid}: {', '.join(added)}")
cl = write_changelog(a.typeid, added, old, new, a.desc, by=by)
print(f"Added to {a.typeid}: {', '.join(added)}" + (f" (by {by})" if by else ""))
print(f"{a.typeid}: template v{old['template_version']}->v{new['template_version']}, "
f"skill v{old['skill_version']}->v{new['skill_version']}")
print(f"Changelog updated: {cl}")

View File

@ -155,7 +155,8 @@ def init_versions(force=False):
# ---------------------------------------------------------------- changelog (read)
CHANGELOG_HEADERS = ["Date", "Typeid", "Skill Version", "Template Version", "Description"]
# "By" = who made the change (the operator). See operator identity below.
CHANGELOG_HEADERS = ["Date", "Typeid", "Skill Version", "Template Version", "Description", "By"]
def read_changelog():
@ -168,9 +169,9 @@ def read_changelog():
for r in ws.iter_rows(min_row=2, values_only=True):
if not r or all(c is None for c in r):
continue
r = list(r) + [None] * (5 - len(r))
r = list(r) + [None] * (len(CHANGELOG_HEADERS) - len(r)) # pad old 5-col rows
out.append({"date": r[0], "typeid": r[1], "skill": r[2],
"template": r[3], "description": r[4]})
"template": r[3], "description": r[4], "by": r[5]})
return out
@ -180,6 +181,61 @@ def changelog_for_typeid(typeid):
return [r for r in read_changelog() if str(r["typeid"]) == str(typeid)]
# ---------------------------------------------------------------- operator identity
# Where each person's "who am I" is stored, on their OWN machine. The skill asks once, writes
# it here, and reads it silently on every later run so nobody has to retype their name. Because
# it lives on each person's computer (not in the shared config), it stays per-person even though
# the Claude account and the Gitea token are shared.
IDENTITY_FILE = os.path.expanduser("~/.library-manager-id")
def parse_author(author):
"""'Name <email>' or 'Name' -> (name, email). Returns (None, None) for empty input."""
if not author:
return None, None
m = re.match(r"^\s*(.*?)\s*<(.*?)>\s*$", str(author))
if m:
return m.group(1).strip() or None, m.group(2).strip()
return str(author).strip() or None, ""
def _config_operator():
"""(name, email) from the OPERATOR / OPERATOR_EMAIL lines of config/gitea.env — the
per-install default identity (e.g. the admin's install sets OPERATOR=admin)."""
cfg = os.path.join(SKILL_ROOT, "config", "gitea.env")
name = email = None
if os.path.exists(cfg):
for line in open(cfg, encoding="utf-8"):
line = line.strip()
if line.startswith("OPERATOR="):
name = line.split("=", 1)[1].strip()
elif line.startswith("OPERATOR_EMAIL="):
email = line.split("=", 1)[1].strip()
return (name or None), (email or "")
def resolve_operator(author_arg=None):
"""Return (name, email) for the person running this, trying in order:
(1) an explicit --author, (2) the LM_AUTHOR_NAME/LM_AUTHOR_EMAIL env vars, (3) the
per-person identity file ~/.library-manager-id, (4) the per-install OPERATOR default in
config/gitea.env. (None, None) if none set callers fall back to the generic bot."""
name, email = parse_author(author_arg)
if name:
return name, email
name = os.environ.get("LM_AUTHOR_NAME")
if name:
return name, os.environ.get("LM_AUTHOR_EMAIL", "")
if os.path.exists(IDENTITY_FILE):
try:
d = json.load(open(IDENTITY_FILE, encoding="utf-8"))
if d.get("name"):
return d.get("name"), d.get("email", "")
except Exception:
pass
return _config_operator()
if __name__ == "__main__":
import sys
if len(sys.argv) > 1 and sys.argv[1] == "init":

View File

@ -53,7 +53,7 @@ def add_version_history(wb, typeid):
ws = wb.create_sheet(VERSION_SHEET)
s = Side(style="thin", color=GRAY)
border = Border(left=s, right=s, top=s, bottom=s)
headers = ["Date", "Skill Version", "Template Version", "Description"]
headers = ["Date", "Skill Version", "Template Version", "Description", "By"]
for c, h in enumerate(headers, start=1):
cell = ws.cell(1, c, h)
cell.font = Font(name="Calibri", bold=True)
@ -62,12 +62,13 @@ def add_version_history(wb, typeid):
cell.alignment = Alignment(horizontal="center", vertical="center")
for row in rows:
r = ws.max_row + 1
vals = [row["date"], _vtrans(row["skill"]), _vtrans(row["template"]), row["description"]]
vals = [row["date"], _vtrans(row["skill"]), _vtrans(row["template"]),
row["description"], row.get("by") or ""]
for c, v in enumerate(vals, start=1):
cell = ws.cell(r, c, v)
cell.border = border
cell.alignment = Alignment(vertical="center", wrap_text=(c == 4), horizontal="left")
for col, w in zip("ABCD", (18, 16, 18, 60)):
for col, w in zip("ABCDE", (18, 16, 18, 50, 22)):
ws.column_dimensions[col].width = w
ws.freeze_panes = "A2"
return True

View File

@ -44,7 +44,8 @@ nothing is written.
import argparse, os, re, shutil, subprocess, sys, tempfile
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from common import (class_folder, load_taxonomy, mpn_make_prefix,
SKILL_ROOT, TEMPLATE_XLSX, VERSIONS_JSON, CHANGELOG_XLSX)
SKILL_ROOT, TEMPLATE_XLSX, VERSIONS_JSON, CHANGELOG_XLSX,
CHANGELOG_HEADERS, resolve_operator)
CFG_DEFAULT = os.path.join(os.path.dirname(__file__), "..", "config", "gitea.env")
@ -89,9 +90,15 @@ def clone(env, dest, repo=None):
return dest
def commit_push(root, token, msg):
for args in (["config", "user.email", "datasheet-bot@local"],
["config", "user.name", "library-manager"],
def commit_push(root, token, msg, author=None):
"""Commit + push. `author` = (name, email) of the operator; when given, the commit is
authored by that real person (visible in git log / Gitea commit view / blame). Without it,
a generic bot identity is used."""
name, email = author or (None, None)
name = name or "library-manager"
email = email or "datasheet-bot@local"
for args in (["config", "user.email", email],
["config", "user.name", name],
["checkout", "-B", "main"], ["add", "-A"]):
subprocess.run(["git", "-C", root] + args, capture_output=True)
if subprocess.run(["git", "-C", root, "diff", "--cached", "--quiet"]).returncode == 0:
@ -193,30 +200,50 @@ def cmd_place_part(env, args):
print(f"placed {tag} -> {cls}/{tag}")
def _operator(args):
"""(name, email) of who's running this — from --author, else env, else identity file."""
return resolve_operator(getattr(args, "author", None))
def _by(name):
return f" (by {name})" if name else ""
def cmd_commit_push(env, args):
commit_push(args.root, env.get("GIT_TOKEN", ""), args.message or "library-manager: update components")
name, email = _operator(args)
commit_push(args.root, env.get("GIT_TOKEN", ""),
args.message or f"library-manager: update components{_by(name)}", author=(name, email))
def cmd_push_part(env, args):
name, email = _operator(args)
tmp = tempfile.mkdtemp(prefix="components_")
clone(env, tmp)
cls, tag, _ = place_part(tmp, args.folder, args.typeid)
print(f"placed {tag} -> {cls}/{tag}")
commit_push(tmp, env.get("GIT_TOKEN", ""), args.message or f"library-manager: add {tag}")
print(f"placed {tag} -> {cls}/{tag}" + (f" (by {name})" if name else ""))
commit_push(tmp, env.get("GIT_TOKEN", ""),
args.message or f"library-manager: add {tag}{_by(name)}", author=(name, email))
# ------------------------------------------------------------------ skill repo + changelog
def _changelog_rows(path):
"""(workbook, sheet, [row-tuples]) for a changelog xlsx; ([]) if the file is absent."""
"""(workbook, sheet, [row-tuples]) for a changelog xlsx; ([]) if the file is absent. Rows are
padded/truncated to the canonical column count so old 5-column changelogs (no 'By') merge
cleanly with new 6-column ones."""
import openpyxl
if not os.path.exists(path):
return None, None, []
n = len(CHANGELOG_HEADERS)
wb = openpyxl.load_workbook(path)
ws = wb["Changelog"] if "Changelog" in wb.sheetnames else wb.active
rows = [tuple("" if c is None else str(c) for c in r)
for r in ws.iter_rows(min_row=2, values_only=True)]
rows = []
for r in ws.iter_rows(min_row=2, values_only=True):
vals = ["" if c is None else str(c) for c in r]
vals = (vals + [""] * n)[:n]
if any(v for v in vals):
rows.append(tuple(vals))
return wb, ws, rows
@ -234,6 +261,7 @@ def merge_changelog(local_path, repo_path):
shutil.copy(local_path, repo_path)
_, _, rows = _changelog_rows(repo_path)
return len(rows)
from openpyxl.styles import Font, PatternFill
repo_wb, repo_ws, repo_rows = _changelog_rows(repo_path)
_, _, local_rows = _changelog_rows(local_path)
seen = set(repo_rows)
@ -245,16 +273,26 @@ def merge_changelog(local_path, repo_path):
seen.add(row)
added += 1
merged.sort(key=lambda r: r[0]) # chronological by Date (column A)
if repo_ws.max_row > 1: # rewrite data rows, keep the header
repo_ws.delete_rows(2, repo_ws.max_row - 1)
n = len(CHANGELOG_HEADERS)
desc_col = CHANGELOG_HEADERS.index("Description") + 1
s = Side(style="thin", color="BFBFBF")
border = Border(left=s, right=s, top=s, bottom=s)
if repo_ws.max_row > 1: # clear everything, rewrite header + rows
repo_ws.delete_rows(1, repo_ws.max_row)
for c, h in enumerate(CHANGELOG_HEADERS, start=1): # canonical header (migrates 5->6 cols)
cell = repo_ws.cell(1, c, h)
cell.font = Font(name="Calibri", bold=True)
cell.fill = PatternFill("solid", fgColor="B6D7A8")
cell.border = border
cell.alignment = Alignment(horizontal="center", vertical="center")
for row in merged:
repo_ws.append(list(row))
rr = repo_ws.max_row
for c in range(1, 6):
repo_ws.cell(rr, c).border = border
repo_ws.cell(rr, c).alignment = Alignment(vertical="center", wrap_text=(c == 5), horizontal="left")
rr = repo_ws.max_row + 1
for c in range(1, n + 1):
cell = repo_ws.cell(rr, c, row[c - 1] if c - 1 < len(row) else "")
cell.border = border
cell.alignment = Alignment(vertical="center", wrap_text=(c == desc_col), horizontal="left")
repo_wb.freeze_panes = None
repo_ws.freeze_panes = "A2"
repo_wb.save(repo_path)
return added
@ -297,7 +335,10 @@ def cmd_push_skill(env, args):
if os.path.exists(repo_cl): # sync merged history back to local
shutil.copy(repo_cl, local_cl)
print(f"changelog: merged (+{added} new row(s)) into the skill repo's CHANGELOG.xlsx")
commit_push(tmp, env.get("GIT_TOKEN", ""), args.message or "library-manager: sync skill files + changelog")
name, email = _operator(args)
commit_push(tmp, env.get("GIT_TOKEN", ""),
args.message or f"library-manager: sync skill files + changelog{_by(name)}",
author=(name, email))
def cmd_pull_skill(env, args):
@ -338,12 +379,14 @@ def main():
p.add_argument("--typeid", required=True); p.add_argument("--root"); p.add_argument("--local")
p = sub.add_parser("commit-push"); p.add_argument("--root", required=True)
p.add_argument("--message")
p.add_argument("--message"); p.add_argument("--author", help="'Name <email>' of the operator")
p = sub.add_parser("push-part"); p.add_argument("--folder", required=True)
p.add_argument("--typeid", required=True); p.add_argument("--message")
p.add_argument("--author", help="'Name <email>' of the operator (who added the part)")
p = sub.add_parser("push-skill")
p.add_argument("--author", help="'Name <email>' of the operator (who made the change)")
p.add_argument("--src", default=os.path.join(os.path.dirname(__file__), ".."),
help="skill directory to push (defaults to this skill)")
p.add_argument("--message")