Overlay native taxonomy JSON and local DeepSeek skills.
repo_paths and compose mount src/taxonomy first. Docker copies dependency then src for taxonomy and skills. SKILL.md prose is rewritten; schema IDs and validate contracts stay. Manifest 1.14.0. Do not run upload_skills.py.
This commit is contained in:
+1
-1
@@ -25,7 +25,7 @@ services:
|
||||
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
- ./periscope/dependency/taxonomy:/app/taxonomy
|
||||
- ./periscope/src/taxonomy:/app/taxonomy
|
||||
|
||||
ports:
|
||||
- "8080:8080"
|
||||
|
||||
@@ -14,7 +14,9 @@ COPY periscope/dependency/backend/ /app/backend/
|
||||
COPY periscope/src/backend/ /app/backend/
|
||||
|
||||
COPY periscope/dependency/taxonomy/ /app/taxonomy/
|
||||
COPY periscope/src/taxonomy/ /app/taxonomy/
|
||||
COPY periscope/dependency/skills/ /app/skills/
|
||||
COPY periscope/src/skills/ /app/skills/
|
||||
COPY periscope/dependency/frontend/content/changelog.md /app/changelog.md
|
||||
COPY vendor/ /app/vendor/
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Periscope living taxonomy: JSON files on disk, dotted subtype keys.
|
||||
|
||||
JSON lives under ``repo_paths.taxonomy_dir()`` (Docker ``/app/taxonomy``, else
|
||||
``periscope/dependency/taxonomy`` until a src tree exists).
|
||||
JSON lives under ``repo_paths.taxonomy_dir()``: ``periscope/src/taxonomy``
|
||||
first, then Docker ``/app/taxonomy``, then the inherited tree.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -4,7 +4,7 @@ Physical split:
|
||||
periscope/src native Periscope
|
||||
periscope/dependency inherited PinScope (in-tree dependency)
|
||||
|
||||
Docker overlays both trees into /app/backend, /app/taxonomy, /app/skills.
|
||||
Docker copies dependency then src for backend, taxonomy, and skills.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -48,22 +48,22 @@ def src_root() -> Path:
|
||||
|
||||
|
||||
def taxonomy_dir() -> Path:
|
||||
app = Path("/app/taxonomy")
|
||||
if app.is_dir() and any(app.glob("*.json")):
|
||||
return app
|
||||
src = src_root() / "taxonomy"
|
||||
if src.is_dir() and any(src.glob("*.json")):
|
||||
return src
|
||||
app = Path("/app/taxonomy")
|
||||
if app.is_dir() and any(app.glob("*.json")):
|
||||
return app
|
||||
return dependency_root() / "taxonomy"
|
||||
|
||||
|
||||
def skills_dir() -> Path:
|
||||
app = Path("/app/skills")
|
||||
if app.is_dir() and (app / "extract-pintable" / "SKILL.md").is_file():
|
||||
return app
|
||||
src = src_root() / "skills"
|
||||
if src.is_dir() and (src / "extract-pintable" / "SKILL.md").is_file():
|
||||
return src
|
||||
app = Path("/app/skills")
|
||||
if app.is_dir() and (app / "extract-pintable" / "SKILL.md").is_file():
|
||||
return app
|
||||
return dependency_root() / "skills"
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"default_model_version": "1.14.0",
|
||||
"extract-pintable": {
|
||||
"skill_id": "skill_01VMWPZuvuZAe4LmLbmsNWNY",
|
||||
"latest_version": "1784798970179642",
|
||||
"display_title": "Extract Pin Table"
|
||||
},
|
||||
"extract-pattern": {
|
||||
"skill_id": "skill_01JuA5xdSJsz2V4dcwzpTRpe",
|
||||
"latest_version": "1784798971057751",
|
||||
"display_title": "Extract Passive Pattern"
|
||||
},
|
||||
"extract-specs": {
|
||||
"skill_id": "skill_01NHZY6K3tvdbAzBo7eGT8qD",
|
||||
"latest_version": "1784798971971891",
|
||||
"display_title": "Extract Component Specs"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
skill_name: extract-pattern
|
||||
description: Pull a resistor/capacitor/inductor MPN pattern from a datasheet PDF. Return the payload through save_pattern.
|
||||
---
|
||||
|
||||
# Passive MPN pattern extraction (Periscope)
|
||||
|
||||
Read the passive datasheet and call `save_pattern`. Do not write files.
|
||||
|
||||
## Where the numbering lives
|
||||
|
||||
Find **Part Numbering System**, **Ordering Information**, or **Explanation of Part No.** It shows field positions, code tables, and a worked example.
|
||||
|
||||
From the front page also take manufacturer, `component_type` (`resistor` | `capacitor` | `inductor`), and series / product name.
|
||||
|
||||
## Fields
|
||||
|
||||
Each field: snake_case `name` (regex group), 0-based `position`, `length`, `description`, and `lookup` (code → meaning). Prefer these names: `size`, `tolerance`, `resistance`, `capacitance`, `inductance`, `voltage`, `wattage`, `dielectric`, `packing_type`, `packing_qty`, `series`, `special`, `thickness`, `reserved`.
|
||||
|
||||
The primary value field (`resistance` / `capacitance` / `inductance`) has `lookup: {}` — it is decoded by algorithm.
|
||||
|
||||
## Value decoder
|
||||
|
||||
Capacitors, 3-digit EIA in pF (`106` → 10 µF):
|
||||
|
||||
```json
|
||||
{"type": "eia3_pf", "base_unit": "pF", "output_unit": "F", "letter_multipliers": {}, "zero_code": null, "conditional_on": null}
|
||||
```
|
||||
|
||||
Resistors, 4-digit code that depends on tolerance:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "eia4_ohm_conditional",
|
||||
"base_unit": "ohm",
|
||||
"output_unit": "ohm",
|
||||
"letter_multipliers": {"J": -1, "K": -2, "L": -3, "M": -4, "N": -5, "P": -6},
|
||||
"zero_code": "0000",
|
||||
"conditional_on": {
|
||||
"field": "tolerance",
|
||||
"high_tolerance": ["J"],
|
||||
"high_tolerance_layout": {"significant_start": 1, "significant_count": 2, "multiplier_index": 3},
|
||||
"low_tolerance_layout": {"significant_start": 0, "significant_count": 3, "multiplier_index": 3}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Read which tolerance codes use 2 vs 3 significant digits, letter multipliers, and jumper/zero codes. Adapt if the PDF uses another scheme.
|
||||
|
||||
## Regex
|
||||
|
||||
Python regex, `^…$`, named groups `(?P<name>…)`. Enumerate known codes (`(?P<size>0603|0805|1206)`), not a loose `\d{4}` for size. Value field: digits plus any letter multipliers.
|
||||
|
||||
## Subtype
|
||||
|
||||
Pick the most specific path from `EXISTING PASSIVE TAXONOMY SUBTYPES`. If none fit, propose `passive.{type}.{specific}`.
|
||||
|
||||
## Checks
|
||||
|
||||
Regex matches every example MPN in the system prompt. Positions + lengths cover the MPN without overlap. Primary value field lookup is empty; other fields have lookups from the PDF. Decoder type matches the part class.
|
||||
|
||||
```bash
|
||||
python3 /skills/extract-pattern/validate.py '<your JSON here>'
|
||||
```
|
||||
|
||||
If that passes, call `save_pattern`.
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"manufacturer": {
|
||||
"type": "string"
|
||||
},
|
||||
"series": {
|
||||
"type": "string"
|
||||
},
|
||||
"component_type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"resistor",
|
||||
"capacitor",
|
||||
"inductor"
|
||||
]
|
||||
},
|
||||
"component_subtype": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"regex": {
|
||||
"type": "string"
|
||||
},
|
||||
"fields": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"position": {
|
||||
"type": "integer"
|
||||
},
|
||||
"length": {
|
||||
"type": "integer"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"lookup": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"position",
|
||||
"length",
|
||||
"description"
|
||||
]
|
||||
}
|
||||
},
|
||||
"value_decoder": {
|
||||
"type": "object"
|
||||
},
|
||||
"example_mpns": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"manufacturer",
|
||||
"series",
|
||||
"component_type",
|
||||
"component_subtype",
|
||||
"description",
|
||||
"regex",
|
||||
"fields",
|
||||
"value_decoder",
|
||||
"example_mpns"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check save_pattern payloads against schema.json and example MPNs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
SCHEMA_PATH = Path(__file__).resolve().parent / "schema.json"
|
||||
_TYPES = frozenset({"resistor", "capacitor", "inductor"})
|
||||
|
||||
|
||||
def validate(data: dict) -> list[str]:
|
||||
errors: list[str] = []
|
||||
schema = json.loads(SCHEMA_PATH.read_text(encoding="utf-8"))
|
||||
for field in schema.get("required", []):
|
||||
if field not in data:
|
||||
errors.append(f"Missing required field: {field}")
|
||||
|
||||
kind = data.get("component_type")
|
||||
if "component_type" in data and kind not in _TYPES:
|
||||
errors.append(f"component_type must be resistor/capacitor/inductor, got: {kind!r}")
|
||||
|
||||
compiled = None
|
||||
if "regex" in data:
|
||||
try:
|
||||
compiled = re.compile(data["regex"])
|
||||
except re.error as exc:
|
||||
errors.append(f"Invalid regex: {exc}")
|
||||
if compiled is not None:
|
||||
for mpn in data.get("example_mpns") or []:
|
||||
if not compiled.match(mpn):
|
||||
errors.append(f"Regex does not match example MPN: {mpn!r}")
|
||||
|
||||
fields = data.get("fields")
|
||||
if "fields" in data:
|
||||
if not isinstance(fields, list) or not fields:
|
||||
errors.append("fields must be a non-empty array")
|
||||
else:
|
||||
for i, field in enumerate(fields):
|
||||
if not isinstance(field, dict):
|
||||
errors.append(f"fields[{i}] must be an object")
|
||||
continue
|
||||
for key in ("name", "position", "length", "description"):
|
||||
if key not in field:
|
||||
errors.append(f"fields[{i}] missing: {key}")
|
||||
|
||||
decoder = data.get("value_decoder")
|
||||
if "value_decoder" in data and (not isinstance(decoder, dict) or "type" not in decoder):
|
||||
errors.append("value_decoder must be an object with a 'type' field")
|
||||
return errors
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python3 validate.py '<json string>'")
|
||||
sys.exit(1)
|
||||
try:
|
||||
payload = json.loads(sys.argv[1])
|
||||
except json.JSONDecodeError as exc:
|
||||
print(f"INVALID JSON: {exc}")
|
||||
sys.exit(1)
|
||||
problems = validate(payload)
|
||||
if problems:
|
||||
print("VALIDATION FAILED:")
|
||||
for item in problems:
|
||||
print(f" - {item}")
|
||||
sys.exit(1)
|
||||
print("VALIDATION PASSED")
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
skill_name: extract-pintable
|
||||
description: Pull pin table, package, abs-max, layout_rules, and IC subtype from a datasheet PDF. Return the payload through save_pintable.
|
||||
---
|
||||
|
||||
# IC pin table extraction (Periscope)
|
||||
|
||||
Read the IC datasheet and call `save_pintable` with structured fields. Do not write files.
|
||||
|
||||
Work in this order: (1) complete pin table for the MPN’s package, (2) `layout_rules` from layout / typical-application pages, (3) package, absolute-maximum ratings, subtype.
|
||||
|
||||
## Datasheet pages to prefer
|
||||
|
||||
Pin configuration, ordering / decoder, package outline, PCB layout / land pattern, typical application (caps, vias, keepouts), absolute maximum ratings.
|
||||
|
||||
## Pin table
|
||||
|
||||
Every pin needs `number` (int, string, or BGA id such as `"A3"`), `name` (verbatim), optional `description`, optional `functions` (mux list).
|
||||
|
||||
Include power, ground, NC, and exposed pad / EP. Do not rename pins. For muxed pins, primary name in `name`, extras in `functions`. If several package tables exist, use the one that matches the MPN. Off-by-one pin numbers break the graph.
|
||||
|
||||
**Modules vs die.** MPNs containing `WROOM`, `WROVER`, `MODULE`, `MOD-`, or `SIP` are modules. Extract the **module landing-pad** table. Do not take the nested SoC/QFN ball map. On Espressif WROOM, pad 1 is GND. Pin 1 named `ANT`, `CHIP_PU`, or `XTAL_*` means the die table — invalid. Crystal, antenna, and flash inside a WROOM can are not schematic pin numbers.
|
||||
|
||||
Optional, only from the block diagram: `internal_features.pullup_pins`, `esd_clamp_pins`, `analog_switch`.
|
||||
|
||||
## layout_rules
|
||||
|
||||
Always scan layout / application / thermal pages. Emit a list. Use `[]` only when those pages have no placement guidance.
|
||||
|
||||
Look under “PCB Layout”, “Layout Guidelines”, “Board Layout”, “Land Pattern”, “Typical Application”, thermal/EP via notes, and figure callouts (“CIN within 2 mm of VIN”).
|
||||
|
||||
Closed `kind` set: `decoupling_proximity`, `thermal_via`, `keepout`, `length_match`, `impedance`, `max_length`, `spacing`, `ref_plane`, `si_via`, `layer`, `series_resistor`, `return_path`, `si`, `emi`, `common_mode`, `shield`.
|
||||
|
||||
Do not invent 50 Ω / 90 Ω rules for I2C, GPIO, EN, analog REGN, or USB CC. Do not invent USB 90 Ω unless this PDF states it.
|
||||
|
||||
SI kinds (`impedance`, `length_match`, `max_length`, `spacing`, `ref_plane`, `si_via`, `layer`, `series_resistor`, `return_path`, `si`) **require** `net_class`: `usb2`, `usb3`, `eth_mdi`, `rgmii`, `sgmii`, `ddr3`, `hdmi`, `pcie`, or `lvds`. PHY+RJ45 → `eth_mdi`; MAC–PHY → `rgmii`/`sgmii`; USB-C SuperSpeed → `usb3`; USB D+/D− → `usb2`.
|
||||
|
||||
`series_resistor` is HS series/termination on that net (USB 22 Ω, RGMII 22 Ω). It is not CHIP_PU / EN / RESET RC, ILIM, or a strap divider.
|
||||
|
||||
Fields: `pin` as printed; `cap_value_hint` only if shown; `max_distance_mm` only when the PDF gives millimetres (“within 2 mm” → `2`). “As close as possible” → `max_distance_mm: null` plus a `note`. Never invent JEDEC/USB/IPC distances. `same_layer` only if the text says same/opposite side. `min_via_count` only if stated. `note` is a short quote. `source_page` is 1-based and required when emitting a rule.
|
||||
|
||||
Example proximity with a millimetre:
|
||||
|
||||
```json
|
||||
{"kind": "decoupling_proximity", "pin": "VIN", "cap_value_hint": "10uF", "max_distance_mm": 2.0, "same_layer": true, "note": "Place CIN within 2 mm of VIN", "source_page": 14}
|
||||
```
|
||||
|
||||
Without a millimetre, still emit the rule with `max_distance_mm` null. Thermal vias use `kind: thermal_via` and `min_via_count` when the PDF gives a count.
|
||||
|
||||
Do not invent land-pattern pad sizes from the mechanical drawing. Do not emit SI kinds without `net_class`. One rule per distinct pin/guidance.
|
||||
|
||||
## Package
|
||||
|
||||
`base_family` (e.g. `MSPM0G3507` from `MSPM0G3507SPTR`), `package` (`LQFP-48`), `pin_count` (int), `description` from ordering / device-information tables.
|
||||
|
||||
## Absolute maximum ratings
|
||||
|
||||
Copy the Absolute Maximum Ratings table (not Recommended Operating Conditions): `parameter`, `min` / `max`, `unit`, `source_page` (1-based). Supplies, pin voltages, input current, temperature. Skip HBM/IEC kV ESD unless that is the only voltage limit. Do not invent numbers.
|
||||
|
||||
For `ic.protection.esd` (and similar), also take Vrwm and polarity/topology from Electrical Characteristics (`unit: "—"` for topology rows).
|
||||
|
||||
## Subtype
|
||||
|
||||
Choose the best dotted path from `EXISTING IC TAXONOMY SUBTYPES` (`ic.mcu`, `ic.power.ldo`, …). If none fit, propose `ic.{category}.{specific}`.
|
||||
|
||||
## Checks
|
||||
|
||||
Pin count matches this MPN’s package. No duplicate or missing numbers. `layout_rules` is present (`[]` only if truly empty). Numeric `max_distance_mm` comes from the PDF. Names are not OCR junk.
|
||||
|
||||
Then:
|
||||
|
||||
```bash
|
||||
python3 /skills/extract-pintable/validate.py '<your JSON here>'
|
||||
```
|
||||
|
||||
If that passes, call `save_pintable`.
|
||||
@@ -0,0 +1,266 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"component_subtype": {
|
||||
"type": "string",
|
||||
"description": "Dotted taxonomy path (ic.mcu, ic.power.ldo)"
|
||||
},
|
||||
"package_info": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"base_family": {
|
||||
"type": "string"
|
||||
},
|
||||
"package": {
|
||||
"type": "string"
|
||||
},
|
||||
"pin_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"base_family",
|
||||
"package",
|
||||
"pin_count"
|
||||
]
|
||||
},
|
||||
"pintable": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"number": {},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"functions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"number",
|
||||
"name"
|
||||
]
|
||||
}
|
||||
},
|
||||
"absolute_maximum_ratings": {
|
||||
"type": "array",
|
||||
"description": "Abs-max rows; ESD/TVS also include Vrwm and polarity.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"parameter": {
|
||||
"type": "string"
|
||||
},
|
||||
"min": {
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"max": {
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"unit": {
|
||||
"type": "string"
|
||||
},
|
||||
"source_page": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"parameter",
|
||||
"unit",
|
||||
"source_page"
|
||||
]
|
||||
}
|
||||
},
|
||||
"internal_features": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"esd_clamp_pins": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"pullup_pins": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"analog_switch": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"layout_rules": {
|
||||
"type": "array",
|
||||
"description": "Layout constraints quoted from the datasheet. Empty list if none.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"decoupling_proximity",
|
||||
"thermal_via",
|
||||
"keepout",
|
||||
"length_match",
|
||||
"impedance",
|
||||
"max_length",
|
||||
"spacing",
|
||||
"ref_plane",
|
||||
"si_via",
|
||||
"layer",
|
||||
"series_resistor",
|
||||
"return_path",
|
||||
"si",
|
||||
"emi",
|
||||
"common_mode",
|
||||
"shield"
|
||||
]
|
||||
},
|
||||
"pin": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"cap_value_hint": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"max_distance_mm": {
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"same_layer": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"min_via_count": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"max_via_count": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"net_class": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"note": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"source_page": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"z0_ohm": {
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"zdiff_ohm": {
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tolerance_pct": {
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"z_min_ohm": {
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"z_max_ohm": {
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"topology": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"min_spacing_mm": {
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"value_ohms": {
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"ref_plane": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"parameter": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"component_subtype",
|
||||
"package_info",
|
||||
"pintable"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check save_pintable payloads against schema.json and module-footprint rules."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
SCHEMA_PATH = Path(__file__).resolve().parent / "schema.json"
|
||||
|
||||
_LAYOUT_KINDS = frozenset({
|
||||
"decoupling_proximity", "thermal_via", "keepout", "length_match",
|
||||
"impedance", "max_length", "spacing", "ref_plane", "si_via",
|
||||
"layer", "series_resistor", "return_path", "si",
|
||||
"emi", "common_mode", "shield",
|
||||
})
|
||||
_SI_KINDS = frozenset({
|
||||
"length_match", "impedance", "max_length", "spacing",
|
||||
"ref_plane", "si_via", "layer", "series_resistor",
|
||||
"return_path", "si",
|
||||
})
|
||||
_MODULE_MPN = re.compile(r"WROOM|WROVER|\bMODULE\b|\bSIP\b", re.I)
|
||||
_STRAP_NOTE = re.compile(r"\b(EN|CHIP_PU|CHIP_EN|STRAP|ILIM)\b", re.I)
|
||||
|
||||
|
||||
def _positive_number(value, label: str, errors: list[str]) -> None:
|
||||
if value is None or value is False:
|
||||
return
|
||||
if isinstance(value, bool):
|
||||
errors.append(f"{label} must be a number or null")
|
||||
return
|
||||
if isinstance(value, (int, float)):
|
||||
if float(value) <= 0:
|
||||
errors.append(f"{label} must be > 0")
|
||||
return
|
||||
try:
|
||||
parsed = float(str(value).strip())
|
||||
except (TypeError, ValueError):
|
||||
errors.append(
|
||||
f"{label} must be numeric or null (got {value!r}) — "
|
||||
"do not invent distances; use null when the PDF only says 'close'"
|
||||
)
|
||||
return
|
||||
if parsed <= 0:
|
||||
errors.append(f"{label} must be > 0")
|
||||
|
||||
|
||||
def _positive_int(value, label: str, errors: list[str]) -> None:
|
||||
if value is None or value is False or isinstance(value, bool):
|
||||
return
|
||||
if isinstance(value, int):
|
||||
if value <= 0:
|
||||
errors.append(f"{label} must be > 0")
|
||||
return
|
||||
try:
|
||||
parsed = int(float(str(value).strip()))
|
||||
except (TypeError, ValueError):
|
||||
errors.append(f"{label} must be an integer or null (got {value!r})")
|
||||
return
|
||||
if parsed <= 0:
|
||||
errors.append(f"{label} must be > 0")
|
||||
|
||||
|
||||
def validate(data: dict) -> list[str]:
|
||||
errors: list[str] = []
|
||||
schema = json.loads(SCHEMA_PATH.read_text(encoding="utf-8"))
|
||||
for field in schema.get("required", []):
|
||||
if field not in data:
|
||||
errors.append(f"Missing required field: {field}")
|
||||
|
||||
subtype = data.get("component_subtype")
|
||||
if "component_subtype" in data and (not isinstance(subtype, str) or "." not in subtype):
|
||||
errors.append(f"component_subtype must be dotted path, got: {subtype!r}")
|
||||
|
||||
pkg = data.get("package_info")
|
||||
if "package_info" in data:
|
||||
if not isinstance(pkg, dict):
|
||||
errors.append("package_info must be an object")
|
||||
else:
|
||||
for key in ("base_family", "package", "pin_count"):
|
||||
if key not in pkg:
|
||||
errors.append(f"package_info missing required field: {key}")
|
||||
if "pin_count" in pkg and not isinstance(pkg["pin_count"], int):
|
||||
errors.append(
|
||||
f"package_info.pin_count must be integer, got: {type(pkg['pin_count']).__name__}"
|
||||
)
|
||||
|
||||
pins = data.get("pintable")
|
||||
if "pintable" in data:
|
||||
if not isinstance(pins, list) or not pins:
|
||||
errors.append("pintable must be a non-empty array")
|
||||
else:
|
||||
numbers: list = []
|
||||
names: dict[str, str] = {}
|
||||
for i, pin in enumerate(pins):
|
||||
if not isinstance(pin, dict):
|
||||
errors.append(f"pintable[{i}] must be an object")
|
||||
continue
|
||||
if "number" not in pin:
|
||||
errors.append(f"pintable[{i}] missing required field: number")
|
||||
if "name" not in pin:
|
||||
errors.append(f"pintable[{i}] missing required field: name")
|
||||
if "number" in pin:
|
||||
numbers.append(pin["number"])
|
||||
names[str(pin["number"])] = str(pin.get("name") or "").upper()
|
||||
dupes = [n for n in set(numbers) if numbers.count(n) > 1]
|
||||
if dupes:
|
||||
errors.append(f"Duplicate pin numbers: {dupes}")
|
||||
pin1 = names.get("1", "")
|
||||
die_like = bool(re.search(r"\bANT\b|^CHIP_PU$|^XTAL", pin1) and any("XTAL" in n for n in names.values()))
|
||||
mpn = str(data.get("mpn") or "")
|
||||
if die_like and _MODULE_MPN.search(mpn):
|
||||
errors.append(
|
||||
"Pin 1 looks like a bare RF SoC ball (ANT/CHIP_PU) with XTAL "
|
||||
"pins in the table. Module footprints (WROOM) use pad 1 = GND; "
|
||||
"extract the module landing-pad table, not the die map."
|
||||
)
|
||||
|
||||
ratings = data.get("absolute_maximum_ratings")
|
||||
if "absolute_maximum_ratings" in data and ratings is not None:
|
||||
if not isinstance(ratings, list):
|
||||
errors.append("absolute_maximum_ratings must be an array")
|
||||
else:
|
||||
for i, row in enumerate(ratings):
|
||||
if not isinstance(row, dict):
|
||||
errors.append(f"absolute_maximum_ratings[{i}] must be an object")
|
||||
continue
|
||||
for key in ("parameter", "unit", "source_page"):
|
||||
if key not in row:
|
||||
errors.append(f"absolute_maximum_ratings[{i}] missing required field: {key}")
|
||||
|
||||
rules = data.get("layout_rules")
|
||||
if "layout_rules" in data and rules is not None:
|
||||
if not isinstance(rules, list):
|
||||
errors.append("layout_rules must be an array")
|
||||
else:
|
||||
for i, row in enumerate(rules):
|
||||
label = f"layout_rules[{i}]"
|
||||
if not isinstance(row, dict):
|
||||
errors.append(f"{label} must be an object")
|
||||
continue
|
||||
kind = row.get("kind")
|
||||
if kind not in _LAYOUT_KINDS:
|
||||
errors.append(f"{label} unknown kind: {kind!r}")
|
||||
continue
|
||||
_positive_number(row.get("max_distance_mm"), f"{label}.max_distance_mm", errors)
|
||||
_positive_int(row.get("min_via_count"), f"{label}.min_via_count", errors)
|
||||
page = row.get("source_page")
|
||||
if page is not None and not isinstance(page, int):
|
||||
errors.append(f"{label}.source_page must be an integer or null")
|
||||
same = row.get("same_layer")
|
||||
if same is not None and not isinstance(same, bool):
|
||||
errors.append(f"{label}.same_layer must be a boolean or null")
|
||||
net_class = row.get("net_class")
|
||||
if kind in _SI_KINDS and not (isinstance(net_class, str) and net_class.strip()):
|
||||
errors.append(
|
||||
f"{label} SI kind {kind!r} requires net_class "
|
||||
"(usb2|usb3|eth_mdi|rgmii|sgmii|ddr3|hdmi|pcie|lvds)"
|
||||
)
|
||||
note = str(row.get("note") or "")
|
||||
pin = str(row.get("pin") or "")
|
||||
if kind == "series_resistor" and (
|
||||
re.search(r"[µu]F", note, re.I) or _STRAP_NOTE.search(f"{note} {pin}")
|
||||
):
|
||||
errors.append(
|
||||
f"{label} series_resistor is HS termination, not EN/CHIP_PU RC or strap"
|
||||
)
|
||||
return errors
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python3 validate.py '<json string>'")
|
||||
sys.exit(1)
|
||||
try:
|
||||
payload = json.loads(sys.argv[1])
|
||||
except json.JSONDecodeError as exc:
|
||||
print(f"INVALID JSON: {exc}")
|
||||
sys.exit(1)
|
||||
problems = validate(payload)
|
||||
if problems:
|
||||
print("VALIDATION FAILED:")
|
||||
for item in problems:
|
||||
print(f" - {item}")
|
||||
sys.exit(1)
|
||||
print("VALIDATION PASSED")
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
skill_name: extract-specs
|
||||
description: Pull pin table, package, and listed electrical specs from a discrete/simple-part datasheet. Return the payload through save_specs.
|
||||
---
|
||||
|
||||
# Discrete / simple-part specs (Periscope)
|
||||
|
||||
Read the datasheet and call `save_specs`. Do not write files.
|
||||
|
||||
Prefer pin configuration, package info, electrical characteristics, and absolute maximum ratings.
|
||||
|
||||
## Subtype
|
||||
|
||||
Choose the best dotted path from the taxonomy list in the system prompt. If none fit, propose a new dotted name.
|
||||
|
||||
## Pin table
|
||||
|
||||
Each pin: `number`, `name` (verbatim, e.g. `"A"`, `"K"`, `"G"`), optional `description`, optional `functions`. Include tab / exposed pad. Match the package variant (SOT-23 pinouts differ). Off-by-one numbers break review.
|
||||
|
||||
## Package
|
||||
|
||||
`base_family` (e.g. `BAT54` from `BAT54S`), `package` (`SOT-23`, `SOD-123`, `TO-220`), `pin_count` (int), `description` decoding the MPN.
|
||||
|
||||
## Specifications
|
||||
|
||||
Extract **only** the names listed under `PARAMETERS TO EXTRACT`. Drop contact material, insulator, processing temperature, orientation, mounting, plating, and anything else not on that list.
|
||||
|
||||
Search electrical tables, abs-max, and application notes. Prefer typical operating values; keep maxima for ratings.
|
||||
|
||||
Use SPICE prefixes with units: `T` `G` `M` `k` `m` `u` `n` `p`. Good: `"30V"`, `"240mV"`, `"500mA"`, `"47mohm"`, `"18pF"`, `"8MHz"`. Bad: `"0.24V"`, `"0.5A"`. Unitless parameters may be numbers (turns ratio, pin count, hFE). Use `null` when the parameter does not apply or is missing.
|
||||
|
||||
Do not infer or calculate. If min/typ/max all matter, include them in the string (`"550mV typ, 850mV max"`). Extra keys are discarded.
|
||||
|
||||
## save_specs payload
|
||||
|
||||
- `component_subtype` — dotted path (`discrete.diode.schottky`)
|
||||
- `component_subtype_description` — short text (needed for a new subtype)
|
||||
- `package_info` — family, package, pin_count, description
|
||||
- `pintable` — all pins
|
||||
- `values` — map of listed parameter names to extracted values
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"component_subtype": {
|
||||
"type": "string",
|
||||
"description": "Dotted taxonomy path (discrete.diode.schottky, connector.usb)",
|
||||
"pattern": "^[a-z][a-z0-9_]+(\\.[a-z][a-z0-9_]+)*$"
|
||||
},
|
||||
"component_subtype_description": {
|
||||
"type": "string"
|
||||
},
|
||||
"package_info": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"base_family": {
|
||||
"type": "string"
|
||||
},
|
||||
"package": {
|
||||
"type": "string"
|
||||
},
|
||||
"pin_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"base_family",
|
||||
"package",
|
||||
"pin_count"
|
||||
]
|
||||
},
|
||||
"pintable": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"number": {},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"functions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"number",
|
||||
"name"
|
||||
]
|
||||
}
|
||||
},
|
||||
"values": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": [
|
||||
"string",
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"component_subtype",
|
||||
"component_subtype_description",
|
||||
"package_info",
|
||||
"pintable",
|
||||
"values"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check save_specs payloads against schema.json."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
SCHEMA_PATH = Path(__file__).resolve().parent / "schema.json"
|
||||
|
||||
|
||||
def validate(data: dict) -> list[str]:
|
||||
errors: list[str] = []
|
||||
schema = json.loads(SCHEMA_PATH.read_text(encoding="utf-8"))
|
||||
for field in schema.get("required", []):
|
||||
if field not in data:
|
||||
errors.append(f"Missing required field: {field}")
|
||||
|
||||
subtype = data.get("component_subtype")
|
||||
if "component_subtype" in data and (not isinstance(subtype, str) or "." not in subtype):
|
||||
errors.append(f"component_subtype must be dotted path, got: {subtype!r}")
|
||||
|
||||
pkg = data.get("package_info")
|
||||
if "package_info" in data:
|
||||
if not isinstance(pkg, dict):
|
||||
errors.append("package_info must be an object")
|
||||
else:
|
||||
for key in ("base_family", "package", "pin_count"):
|
||||
if key not in pkg:
|
||||
errors.append(f"package_info missing required field: {key}")
|
||||
if "pin_count" in pkg and not isinstance(pkg["pin_count"], int):
|
||||
errors.append(
|
||||
f"package_info.pin_count must be integer, got: {type(pkg['pin_count']).__name__}"
|
||||
)
|
||||
|
||||
pins = data.get("pintable")
|
||||
if "pintable" in data:
|
||||
if not isinstance(pins, list) or not pins:
|
||||
errors.append("pintable must be a non-empty array")
|
||||
else:
|
||||
numbers: list = []
|
||||
for i, pin in enumerate(pins):
|
||||
if not isinstance(pin, dict):
|
||||
errors.append(f"pintable[{i}] must be an object")
|
||||
continue
|
||||
if "number" not in pin:
|
||||
errors.append(f"pintable[{i}] missing required field: number")
|
||||
if "name" not in pin:
|
||||
errors.append(f"pintable[{i}] missing required field: name")
|
||||
if "number" in pin:
|
||||
numbers.append(pin["number"])
|
||||
dupes = [n for n in set(numbers) if numbers.count(n) > 1]
|
||||
if dupes:
|
||||
errors.append(f"Duplicate pin numbers: {dupes}")
|
||||
|
||||
values = data.get("values")
|
||||
if "values" in data:
|
||||
if not isinstance(values, dict):
|
||||
errors.append(f"values must be an object, got: {type(values).__name__}")
|
||||
else:
|
||||
for key, value in values.items():
|
||||
if value is not None and not isinstance(value, (str, int, float)):
|
||||
errors.append(
|
||||
f"values[{key!r}] must be string, number, or null, got: {type(value).__name__}"
|
||||
)
|
||||
return errors
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python3 validate.py '<json string>'")
|
||||
sys.exit(1)
|
||||
try:
|
||||
payload = json.loads(sys.argv[1])
|
||||
except json.JSONDecodeError as exc:
|
||||
print(f"INVALID JSON: {exc}")
|
||||
sys.exit(1)
|
||||
problems = validate(payload)
|
||||
if problems:
|
||||
print("VALIDATION FAILED:")
|
||||
for item in problems:
|
||||
print(f" - {item}")
|
||||
sys.exit(1)
|
||||
print("VALIDATION PASSED")
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"type": "connector",
|
||||
"specs": [
|
||||
{
|
||||
"name": "pin_count",
|
||||
"description": "Contact count",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "voltage_rating_v",
|
||||
"description": "Voltage rating",
|
||||
"unit": "V"
|
||||
},
|
||||
{
|
||||
"name": "current_rating_a",
|
||||
"description": "Current per contact",
|
||||
"unit": "A"
|
||||
}
|
||||
],
|
||||
"subtypes": {
|
||||
"connector.header": {
|
||||
"description": "Pin header",
|
||||
"extra_specs": [
|
||||
{
|
||||
"name": "pitch_mm",
|
||||
"description": "Contact pitch",
|
||||
"unit": "mm"
|
||||
},
|
||||
{
|
||||
"name": "rows",
|
||||
"description": "Row count"
|
||||
},
|
||||
{
|
||||
"name": "positions_per_row",
|
||||
"description": "Positions per row"
|
||||
}
|
||||
]
|
||||
},
|
||||
"connector.usb": {
|
||||
"description": "USB connector",
|
||||
"extra_specs": [
|
||||
{
|
||||
"name": "usb_standard",
|
||||
"description": "USB generation (2.0, 3.x, Type-C)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"connector.fpc": {
|
||||
"description": "FPC / FFC connector",
|
||||
"extra_specs": [
|
||||
{
|
||||
"name": "pitch_mm",
|
||||
"description": "Contact pitch",
|
||||
"unit": "mm"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"type": "crystal",
|
||||
"specs": [
|
||||
{
|
||||
"name": "frequency_hz",
|
||||
"description": "Nominal frequency",
|
||||
"unit": "Hz",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "load_capacitance_f",
|
||||
"description": "Load capacitance CL",
|
||||
"unit": "F"
|
||||
},
|
||||
{
|
||||
"name": "esr_ohm",
|
||||
"description": "ESR",
|
||||
"unit": "ohm"
|
||||
}
|
||||
],
|
||||
"subtypes": {
|
||||
"crystal": {
|
||||
"description": "Crystal or crystal oscillator",
|
||||
"extra_specs": [
|
||||
{
|
||||
"name": "frequency_stability_ppm",
|
||||
"description": "Stability / tolerance",
|
||||
"unit": "ppm"
|
||||
},
|
||||
{
|
||||
"name": "drive_level_w",
|
||||
"description": "Max drive level",
|
||||
"unit": "W"
|
||||
},
|
||||
{
|
||||
"name": "shunt_capacitance_f",
|
||||
"description": "Shunt C0",
|
||||
"unit": "F"
|
||||
}
|
||||
]
|
||||
},
|
||||
"crystal.crystal": {
|
||||
"description": "Crystal or crystal oscillator",
|
||||
"example_mpn": "ABM8-19.200MHZ-10-1-U-T"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
{
|
||||
"type": "discrete",
|
||||
"specs": [
|
||||
{
|
||||
"name": "package",
|
||||
"description": "Package (SOD-123, SOT-23, TO-220)"
|
||||
},
|
||||
{
|
||||
"name": "power_dissipation_w",
|
||||
"description": "Maximum dissipation",
|
||||
"unit": "W"
|
||||
}
|
||||
],
|
||||
"subtypes": {
|
||||
"discrete.diode.rectifier": {
|
||||
"description": "Rectifier diode",
|
||||
"extra_specs": [
|
||||
{
|
||||
"name": "reverse_voltage_v",
|
||||
"description": "Vr / Vrrm",
|
||||
"unit": "V",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "forward_voltage_v",
|
||||
"description": "Typical Vf",
|
||||
"unit": "V"
|
||||
},
|
||||
{
|
||||
"name": "forward_current_a",
|
||||
"description": "Continuous If",
|
||||
"unit": "A"
|
||||
}
|
||||
]
|
||||
},
|
||||
"discrete.diode.schottky": {
|
||||
"description": "Schottky diode",
|
||||
"extra_specs": [
|
||||
{
|
||||
"name": "reverse_voltage_v",
|
||||
"description": "Vr",
|
||||
"unit": "V",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "forward_voltage_v",
|
||||
"description": "Typical Vf",
|
||||
"unit": "V"
|
||||
},
|
||||
{
|
||||
"name": "forward_current_a",
|
||||
"description": "Continuous If",
|
||||
"unit": "A"
|
||||
}
|
||||
]
|
||||
},
|
||||
"discrete.diode.zener": {
|
||||
"description": "Zener regulator diode",
|
||||
"extra_specs": [
|
||||
{
|
||||
"name": "zener_voltage_v",
|
||||
"description": "Nominal Vz",
|
||||
"unit": "V",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "zener_impedance_ohm",
|
||||
"description": "Zzt",
|
||||
"unit": "ohm"
|
||||
}
|
||||
]
|
||||
},
|
||||
"discrete.diode.tvs": {
|
||||
"description": "TVS diode",
|
||||
"extra_specs": [
|
||||
{
|
||||
"name": "standoff_voltage_v",
|
||||
"description": "Vrwm",
|
||||
"unit": "V",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "clamping_voltage_v",
|
||||
"description": "Clamp at Ipp",
|
||||
"unit": "V"
|
||||
},
|
||||
{
|
||||
"name": "peak_pulse_current_a",
|
||||
"description": "Ipp",
|
||||
"unit": "A"
|
||||
}
|
||||
]
|
||||
},
|
||||
"discrete.diode.esd": {
|
||||
"description": "ESD array for data lines",
|
||||
"example_mpn": "USBLC6-2SC6",
|
||||
"extra_specs": [
|
||||
{
|
||||
"name": "standoff_voltage_v",
|
||||
"description": "Vrwm",
|
||||
"unit": "V",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "clamping_voltage_v",
|
||||
"description": "Clamp voltage",
|
||||
"unit": "V"
|
||||
},
|
||||
{
|
||||
"name": "io_capacitance_f",
|
||||
"description": "I/O capacitance Cio",
|
||||
"unit": "F"
|
||||
},
|
||||
{
|
||||
"name": "leakage_current_a",
|
||||
"description": "Reverse leakage IR",
|
||||
"unit": "A"
|
||||
}
|
||||
]
|
||||
},
|
||||
"discrete.transistor.mosfet.n_channel": {
|
||||
"description": "N-channel MOSFET",
|
||||
"extra_specs": [
|
||||
{
|
||||
"name": "vds_max_v",
|
||||
"description": "Vds max",
|
||||
"unit": "V",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "id_max_a",
|
||||
"description": "Id max",
|
||||
"unit": "A"
|
||||
},
|
||||
{
|
||||
"name": "rds_on_ohm",
|
||||
"description": "Rds(on)",
|
||||
"unit": "ohm"
|
||||
},
|
||||
{
|
||||
"name": "vgs_th_v",
|
||||
"description": "Vgs(th)",
|
||||
"unit": "V"
|
||||
},
|
||||
{
|
||||
"name": "qg_c",
|
||||
"description": "Total Qg",
|
||||
"unit": "C"
|
||||
}
|
||||
]
|
||||
},
|
||||
"discrete.transistor.mosfet.p_channel": {
|
||||
"description": "P-channel MOSFET",
|
||||
"extra_specs": [
|
||||
{
|
||||
"name": "vds_max_v",
|
||||
"description": "Vds max",
|
||||
"unit": "V",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "id_max_a",
|
||||
"description": "Id max",
|
||||
"unit": "A"
|
||||
},
|
||||
{
|
||||
"name": "rds_on_ohm",
|
||||
"description": "Rds(on)",
|
||||
"unit": "ohm"
|
||||
},
|
||||
{
|
||||
"name": "vgs_th_v",
|
||||
"description": "Vgs(th)",
|
||||
"unit": "V"
|
||||
}
|
||||
]
|
||||
},
|
||||
"discrete.transistor.bjt.npn": {
|
||||
"description": "NPN BJT",
|
||||
"extra_specs": [
|
||||
{
|
||||
"name": "vce_max_v",
|
||||
"description": "Vce max",
|
||||
"unit": "V",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "ic_max_a",
|
||||
"description": "Ic max",
|
||||
"unit": "A"
|
||||
},
|
||||
{
|
||||
"name": "hfe",
|
||||
"description": "hFE"
|
||||
}
|
||||
]
|
||||
},
|
||||
"discrete.transistor.bjt.pnp": {
|
||||
"description": "PNP BJT",
|
||||
"extra_specs": [
|
||||
{
|
||||
"name": "vce_max_v",
|
||||
"description": "Vce max",
|
||||
"unit": "V",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "ic_max_a",
|
||||
"description": "Ic max",
|
||||
"unit": "A"
|
||||
},
|
||||
{
|
||||
"name": "hfe",
|
||||
"description": "hFE"
|
||||
}
|
||||
]
|
||||
},
|
||||
"discrete.led": {
|
||||
"description": "LED",
|
||||
"extra_specs": [
|
||||
{
|
||||
"name": "forward_voltage_v",
|
||||
"description": "Typical Vf",
|
||||
"unit": "V"
|
||||
},
|
||||
{
|
||||
"name": "forward_current_a",
|
||||
"description": "Typical or max If",
|
||||
"unit": "A"
|
||||
},
|
||||
{
|
||||
"name": "color",
|
||||
"description": "Color or wavelength"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"type": "fuse",
|
||||
"specs": [
|
||||
{
|
||||
"name": "current_rating_a",
|
||||
"description": "Rated current",
|
||||
"unit": "A",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "voltage_rating_v",
|
||||
"description": "Voltage rating",
|
||||
"unit": "V"
|
||||
},
|
||||
{
|
||||
"name": "breaking_capacity_a",
|
||||
"description": "Breaking / interrupting capacity",
|
||||
"unit": "A"
|
||||
}
|
||||
],
|
||||
"subtypes": {
|
||||
"fuse": {
|
||||
"description": "Generic fuse"
|
||||
},
|
||||
"fuse.standard": {
|
||||
"description": "One-shot fuse"
|
||||
},
|
||||
"fuse.ptc_resettable": {
|
||||
"description": "PTC / polyfuse",
|
||||
"extra_specs": [
|
||||
{
|
||||
"name": "hold_current_a",
|
||||
"description": "Ihold",
|
||||
"unit": "A"
|
||||
},
|
||||
{
|
||||
"name": "trip_current_a",
|
||||
"description": "Itrip",
|
||||
"unit": "A"
|
||||
},
|
||||
{
|
||||
"name": "resistance_ohm",
|
||||
"description": "Typical R at 25 C",
|
||||
"unit": "ohm"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"type": "ic",
|
||||
"subtypes": {
|
||||
"ic.mcu": {
|
||||
"description": "Microcontroller",
|
||||
"example_mpn": "MSPM0G3507SPTR"
|
||||
},
|
||||
"ic.power.ldo": {
|
||||
"description": "Low-dropout linear regulator",
|
||||
"example_mpn": "SPX3819M5-L-3-3"
|
||||
},
|
||||
"ic.power.switching_regulator": {
|
||||
"description": "Switching regulator (buck, boost, or buck-boost)"
|
||||
},
|
||||
"ic.power.pmic": {
|
||||
"description": "Multi-rail power-management IC with sequencing"
|
||||
},
|
||||
"ic.interface.usb_uart_bridge": {
|
||||
"description": "USB\u2013UART bridge",
|
||||
"example_mpn": "CH340E"
|
||||
},
|
||||
"ic.interface.level_shifter": {
|
||||
"description": "Logic-level translator"
|
||||
},
|
||||
"ic.interface.can_transceiver": {
|
||||
"description": "CAN transceiver"
|
||||
},
|
||||
"ic.interface.rs485_transceiver": {
|
||||
"description": "RS-485 / RS-422 transceiver"
|
||||
},
|
||||
"ic.protection.esd": {
|
||||
"description": "ESD / TVS protection IC",
|
||||
"example_mpn": "USBLC6-2SC6"
|
||||
},
|
||||
"ic.sensor.accelerometer": {
|
||||
"description": "Accelerometer or IMU"
|
||||
},
|
||||
"ic.sensor.temperature": {
|
||||
"description": "Temperature-sensor IC"
|
||||
},
|
||||
"ic.memory.flash": {
|
||||
"description": "NOR or NAND flash"
|
||||
},
|
||||
"ic.memory.eeprom": {
|
||||
"description": "EEPROM"
|
||||
},
|
||||
"ic.logic.buffer": {
|
||||
"description": "Buffer or line driver"
|
||||
},
|
||||
"ic.logic.gate": {
|
||||
"description": "Combinational logic gate IC"
|
||||
},
|
||||
"ic.amplifier.opamp": {
|
||||
"description": "Operational amplifier"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
{
|
||||
"type": "passive",
|
||||
"specs": [
|
||||
{
|
||||
"name": "value_formatted",
|
||||
"description": "SI-prefixed value string (4.7 kohm, 100 nF)"
|
||||
},
|
||||
{
|
||||
"name": "tolerance",
|
||||
"description": "Tolerance as printed (\u00b11%, \u00b110%)"
|
||||
},
|
||||
{
|
||||
"name": "package",
|
||||
"description": "Chip size code (0603, 0805, 1206)"
|
||||
}
|
||||
],
|
||||
"subtypes": {
|
||||
"passive.resistor": {
|
||||
"description": "Chip resistor",
|
||||
"example_mpn": "0603WAF5101T5E",
|
||||
"extra_specs": [
|
||||
{
|
||||
"name": "value_ohms",
|
||||
"description": "Resistance",
|
||||
"unit": "ohm",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "power_rating_w",
|
||||
"description": "Power rating",
|
||||
"unit": "W"
|
||||
}
|
||||
]
|
||||
},
|
||||
"passive.resistor.thick_film": {
|
||||
"description": "Thick-film chip resistor",
|
||||
"extra_specs": [
|
||||
{
|
||||
"name": "value_ohms",
|
||||
"description": "Resistance",
|
||||
"unit": "ohm",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "power_rating_w",
|
||||
"description": "Power rating",
|
||||
"unit": "W"
|
||||
}
|
||||
]
|
||||
},
|
||||
"passive.resistor.thin_film": {
|
||||
"description": "Thin-film chip resistor",
|
||||
"extra_specs": [
|
||||
{
|
||||
"name": "value_ohms",
|
||||
"description": "Resistance",
|
||||
"unit": "ohm",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "power_rating_w",
|
||||
"description": "Power rating",
|
||||
"unit": "W"
|
||||
}
|
||||
]
|
||||
},
|
||||
"passive.capacitor.ceramic": {
|
||||
"description": "MLCC",
|
||||
"example_mpn": "CL10B474KA8NNNC",
|
||||
"extra_specs": [
|
||||
{
|
||||
"name": "value_farads",
|
||||
"description": "Capacitance",
|
||||
"unit": "F",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "voltage_rating_v",
|
||||
"description": "DC voltage rating",
|
||||
"unit": "V"
|
||||
},
|
||||
{
|
||||
"name": "dielectric",
|
||||
"description": "Dielectric (X7R, C0G, X5R)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"passive.capacitor.tantalum": {
|
||||
"description": "Tantalum capacitor",
|
||||
"extra_specs": [
|
||||
{
|
||||
"name": "value_farads",
|
||||
"description": "Capacitance",
|
||||
"unit": "F",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "voltage_rating_v",
|
||||
"description": "DC voltage rating",
|
||||
"unit": "V"
|
||||
}
|
||||
]
|
||||
},
|
||||
"passive.capacitor.electrolytic": {
|
||||
"description": "Aluminum electrolytic capacitor",
|
||||
"extra_specs": [
|
||||
{
|
||||
"name": "value_farads",
|
||||
"description": "Capacitance",
|
||||
"unit": "F",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "voltage_rating_v",
|
||||
"description": "DC voltage rating",
|
||||
"unit": "V"
|
||||
}
|
||||
]
|
||||
},
|
||||
"passive.inductor": {
|
||||
"description": "Inductor or choke",
|
||||
"extra_specs": [
|
||||
{
|
||||
"name": "value_henries",
|
||||
"description": "Inductance",
|
||||
"unit": "H",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "current_rating_a",
|
||||
"description": "Saturation or rated current",
|
||||
"unit": "A"
|
||||
},
|
||||
{
|
||||
"name": "dcr_ohms",
|
||||
"description": "DC resistance",
|
||||
"unit": "ohm"
|
||||
}
|
||||
]
|
||||
},
|
||||
"passive.ferrite_bead": {
|
||||
"description": "Ferrite bead",
|
||||
"extra_specs": [
|
||||
{
|
||||
"name": "impedance_ohm",
|
||||
"description": "Impedance at the stated test frequency",
|
||||
"unit": "ohm",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "current_rating_a",
|
||||
"description": "Rated current",
|
||||
"unit": "A"
|
||||
},
|
||||
{
|
||||
"name": "dcr_ohms",
|
||||
"description": "DC resistance",
|
||||
"unit": "ohm"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"type": "switch",
|
||||
"specs": [
|
||||
{
|
||||
"name": "voltage_rating_v",
|
||||
"description": "Voltage rating",
|
||||
"unit": "V"
|
||||
},
|
||||
{
|
||||
"name": "current_rating_a",
|
||||
"description": "Current rating",
|
||||
"unit": "A"
|
||||
}
|
||||
],
|
||||
"subtypes": {
|
||||
"switch.tactile": {
|
||||
"description": "Tactile push-button",
|
||||
"extra_specs": [
|
||||
{
|
||||
"name": "contact_configuration",
|
||||
"description": "Contact form (SPST-NO, SPST-NC)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"switch.dip": {
|
||||
"description": "DIP switch",
|
||||
"extra_specs": [
|
||||
{
|
||||
"name": "positions",
|
||||
"description": "Independent positions"
|
||||
},
|
||||
{
|
||||
"name": "contact_configuration",
|
||||
"description": "Form per position (SPST)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"switch.slide": {
|
||||
"description": "Slide switch",
|
||||
"extra_specs": [
|
||||
{
|
||||
"name": "contact_configuration",
|
||||
"description": "Form (SPDT, DPDT)"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"type": "test_point",
|
||||
"subtypes": {
|
||||
"test_point": {
|
||||
"description": "Test point"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"type": "transformer",
|
||||
"specs": [
|
||||
{
|
||||
"name": "turns_ratio",
|
||||
"description": "Primary:secondary turns"
|
||||
},
|
||||
{
|
||||
"name": "voltage_primary_v",
|
||||
"description": "Primary voltage",
|
||||
"unit": "V"
|
||||
},
|
||||
{
|
||||
"name": "voltage_secondary_v",
|
||||
"description": "Secondary voltage",
|
||||
"unit": "V"
|
||||
},
|
||||
{
|
||||
"name": "current_rating_a",
|
||||
"description": "Current rating",
|
||||
"unit": "A"
|
||||
}
|
||||
],
|
||||
"subtypes": {
|
||||
"transformer.power": {
|
||||
"description": "Power transformer",
|
||||
"extra_specs": [
|
||||
{
|
||||
"name": "power_rating_w",
|
||||
"description": "Power rating",
|
||||
"unit": "W"
|
||||
},
|
||||
{
|
||||
"name": "isolation_voltage_v",
|
||||
"description": "Isolation between windings",
|
||||
"unit": "V"
|
||||
}
|
||||
]
|
||||
},
|
||||
"transformer.signal": {
|
||||
"description": "Signal / isolation transformer",
|
||||
"extra_specs": [
|
||||
{
|
||||
"name": "isolation_voltage_v",
|
||||
"description": "Isolation between windings",
|
||||
"unit": "V"
|
||||
},
|
||||
{
|
||||
"name": "insertion_loss_db",
|
||||
"description": "Insertion loss",
|
||||
"unit": "dB"
|
||||
},
|
||||
{
|
||||
"name": "bandwidth_hz",
|
||||
"description": "-3 dB bandwidth",
|
||||
"unit": "Hz"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -6,4 +6,4 @@ from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
SIMPLE_PROJECT = REPO_ROOT / "periscope" / "dependency" / "simple_project"
|
||||
TAXONOMY = REPO_ROOT / "periscope" / "dependency" / "taxonomy"
|
||||
TAXONOMY = REPO_ROOT / "periscope" / "src" / "taxonomy"
|
||||
|
||||
@@ -28,12 +28,11 @@ def test_taxonomy_module_is_src():
|
||||
assert "Native Periscope overlay" not in text
|
||||
|
||||
|
||||
def test_taxonomy_dir_points_at_dependency_json():
|
||||
def test_taxonomy_dir_points_at_src_json():
|
||||
d = taxonomy_dir()
|
||||
assert d.parts[-2:] == ("dependency", "taxonomy")
|
||||
assert d.parts[-2:] == ("src", "taxonomy")
|
||||
assert (d / "ic.json").is_file()
|
||||
assert taxonomy.TAXONOMY_DIR == d
|
||||
assert not (Path(taxonomy.__file__).resolve().parent.parent.parent / "taxonomy" / "ic.json").is_file()
|
||||
|
||||
|
||||
def test_emmaforo_ref_prefixes():
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"""Native taxonomy JSON and local DeepSeek skills live under periscope/src."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from backend.repo_paths import skills_dir, taxonomy_dir
|
||||
from backend.services.llm.local_skill import load_skill_markdown, load_skill_validator
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def test_taxonomy_json_is_src():
|
||||
d = taxonomy_dir()
|
||||
assert d.parts[-2:] == ("src", "taxonomy")
|
||||
for name in (
|
||||
"ic.json",
|
||||
"passive.json",
|
||||
"discrete.json",
|
||||
"connector.json",
|
||||
"crystal.json",
|
||||
"fuse.json",
|
||||
"switch.json",
|
||||
"test_point.json",
|
||||
"transformer.json",
|
||||
):
|
||||
assert (d / name).is_file(), name
|
||||
|
||||
|
||||
def test_skills_dir_is_src_and_local():
|
||||
d = skills_dir()
|
||||
assert d.parts[-2:] == ("src", "skills")
|
||||
for name in ("extract-pintable", "extract-pattern", "extract-specs"):
|
||||
assert (d / name / "SKILL.md").is_file()
|
||||
assert (d / name / "validate.py").is_file()
|
||||
assert (d / name / "schema.json").is_file()
|
||||
text = (d / name / "SKILL.md").read_text(encoding="utf-8")
|
||||
assert "Native Periscope overlay" not in text[:400]
|
||||
assert "upload_skills" not in text
|
||||
|
||||
|
||||
def test_pintable_skill_mentions_save_tool_and_wroom():
|
||||
md = load_skill_markdown("extract-pintable")
|
||||
assert "save_pintable" in md
|
||||
assert "layout_rules" in md
|
||||
assert "WROOM" in md
|
||||
assert "net_class" in md
|
||||
|
||||
|
||||
def test_docker_copies_src_taxonomy_and_skills():
|
||||
docker = (ROOT / "periscope" / "src" / "backend" / "Dockerfile").read_text(encoding="utf-8")
|
||||
assert "COPY periscope/src/taxonomy/ /app/taxonomy/" in docker
|
||||
assert "COPY periscope/src/skills/ /app/skills/" in docker
|
||||
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
|
||||
assert "./periscope/src/taxonomy:/app/taxonomy" in compose
|
||||
assert "dependency/taxonomy:/app/taxonomy" not in compose
|
||||
|
||||
|
||||
def test_skills_manifest_bumped_for_prompt_change():
|
||||
text = (ROOT / "periscope" / "src" / "backend" / "skills_manifest.json").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert '"default_model_version": "1.14.0"' in text
|
||||
validate = load_skill_validator("extract-pintable")
|
||||
assert validate is not None
|
||||
Reference in New Issue
Block a user