Strengthen pintable layout_rules skill and refresh empty extractions.

Treat PCB/application layout guidance as a first-class extract, keep those pages in PDF trim, and re-run IC extraction once when rules are still empty under older model_version.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-13 18:01:15 +02:00
co-authored by Cursor
parent 86121e6547
commit 21e2be1cd0
10 changed files with 332 additions and 64 deletions
+33
View File
@@ -4,6 +4,8 @@ from __future__ import annotations
from typing import Any
from packaging.version import Version
KNOWN_KINDS = frozenset({"decoupling_proximity", "thermal_via", "keepout", "length_match"})
@@ -20,6 +22,37 @@ def _num(v: Any) -> float | None:
return None
def has_any_layout_rule(raw: object) -> bool:
"""True when extraction already produced at least one structured rule."""
if not isinstance(raw, list):
return False
for row in raw:
if isinstance(row, dict) and str(row.get("kind") or "").strip() in KNOWN_KINDS:
return True
return False
def needs_layout_rules_refresh(
data: dict,
*,
min_scan_version: str,
) -> bool:
"""True when layout_rules are empty and the extract predates the scan version.
After a successful extract at ``min_scan_version`` or newer, an empty
``layout_rules`` list means the datasheet had no guidance — do not loop.
"""
if has_any_layout_rule(data.get("layout_rules")):
return False
ver = str(data.get("model_version") or "0.0.0")
if not min_scan_version or min_scan_version == "0.0.0":
return False
try:
return Version(ver) < Version(min_scan_version)
except Exception:
return True
def validate_layout_rules(raw: list | None) -> tuple[list[dict], list[str]]:
"""Return (normalized rows, errors). Empty list is a valid skip."""
if not raw:
+43 -4
View File
@@ -135,8 +135,36 @@ PINTABLE_TOOL = {
},
"layout_rules": {
"type": "array",
"description": "Optional PCB layout constraints from typical-application pages. kind must be decoupling_proximity, thermal_via, keepout, or length_match. max_distance_mm only if the PDF states a number — never invent 3 mm or 3W.",
"items": {"type": "object"},
"description": (
"PCB layout constraints from typical-application / PCB layout pages. "
"kind: decoupling_proximity | thermal_via | keepout | length_match. "
"Fields: pin, cap_value_hint, max_distance_mm (ONLY if the PDF states a "
"number — never invent 3 mm/JEDEC), same_layer (bool), min_via_count, "
"net_class, note, source_page. Empty array if the PDF has no layout guidance."
),
"items": {
"type": "object",
"properties": {
"kind": {
"type": "string",
"enum": [
"decoupling_proximity",
"thermal_via",
"keepout",
"length_match",
],
},
"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"]},
"net_class": {"type": ["string", "null"]},
"note": {"type": ["string", "null"]},
"source_page": {"type": ["integer", "null"]},
},
"required": ["kind"],
},
},
},
"required": ["component_subtype", "component_subtype_description", "package_info", "pintable"],
@@ -257,11 +285,17 @@ _MAX_PDF_PAGES = 120
log = logging.getLogger(__name__)
# Keywords used to find relevant pages for each extraction stage.
# Include PCB / typical-application pages so layout_rules can be extracted
# when large datasheets are trimmed to ≤_MAX_PDF_PAGES.
_PINTABLE_KEYWORDS = re.compile(
r"pin\s*(out|diagram|configuration|description|assignment|function|name|table|map)"
r"|ball\s*map|package\s*(pin|drawing|outline)|signal\s+description"
r"|absolute\s+maximum|recommended\s+operating|electrical\s+characteristics"
r"|ordering\s+information|device\s+information",
r"|ordering\s+information|device\s+information"
r"|pcb\s+layout|layout\s+(guideline|recommendation|consideration|hint)"
r"|typical\s+application|application\s+(circuit|schematic|information|note)"
r"|reference\s+design|decoupling|bypass\s+capacitor|thermal\s+via"
r"|land\s+pattern|keep[\s\-]?out|place\s+(close|near|within)",
re.IGNORECASE,
)
@@ -596,7 +630,12 @@ async def extract_pintable(
skill_name="extract-pintable",
model=model,
system=system,
user_text=f"Extract pin table and package info for MPN: {mpn}",
user_text=(
f"Extract pin table, package info, absolute maximum ratings, "
f"and layout_rules (scan PCB layout / typical application / "
f"thermal pages; max_distance_mm only if the PDF states mm) "
f"for MPN: {mpn}"
),
pdf_path=trimmed,
output_tool=_to_tool(PINTABLE_TOOL),
)
+21 -3
View File
@@ -762,6 +762,10 @@ async def _stage_ic_extraction(ctx: PipelineContext) -> None:
extracted_dir = ctx.ws.local_path("extracted")
# Pre-categorize: workspace cache, library cache, or needs extraction
from backend.config import settings as app_settings
from backend.pinscopex.layout_rules import needs_layout_rules_refresh
layout_scan_ver = app_settings.get_default_model_version()
_ic_cache: dict[str, tuple] = {}
_ic_new_count = 0
for mpn in ctx.ic_mpns:
@@ -771,13 +775,27 @@ async def _stage_ic_extraction(ctx: PipelineContext) -> None:
existing = json.loads(json_path.read_text())
if existing.get("pintable"):
ws_ver = existing.get("model_version", "0.0.0")
if not settings_svc.version_is_stale(ws_ver, ctx.min_ver):
if (
not settings_svc.version_is_stale(ws_ver, ctx.min_ver)
and not needs_layout_rules_refresh(
existing, min_scan_version=layout_scan_ver,
)
):
_ic_cache[mpn] = ("workspace",)
continue
lib_key = proj_svc.library_has_extraction(ctx.storage, mpn, min_version=ctx.min_ver)
if lib_key:
_ic_cache[mpn] = ("library", lib_key)
continue
# Library hit may still lack layout_rules under the current skill.
try:
lib_payload = ctx.storage.read_json(lib_key)
except Exception:
lib_payload = {}
if not needs_layout_rules_refresh(
lib_payload if isinstance(lib_payload, dict) else {},
min_scan_version=layout_scan_ver,
):
_ic_cache[mpn] = ("library", lib_key)
continue
_ic_new_count += 1
broker.publish(ctx.project_id, "step_update",
+1 -1
View File
@@ -1,5 +1,5 @@
{
"default_model_version": "1.9.0",
"default_model_version": "1.10.0",
"extract-pintable": {
"skill_id": "skill_01VMWPZuvuZAe4LmLbmsNWNY",
"latest_version": "1784798970179642",