Ship Sprint 0 CAD foundations so findings, KiCad hierarchy, BOM match, eval, and optional PCB ingest have a stable contract.

Extend Finding with rule_id/net/pins, flatten multi-sheet .kicad_sch, flag MPN mismatches, score simple_project, and parse .kicad_pcb without running layout DRC.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-10 21:54:37 +02:00
co-authored by Cursor
parent d31c04ba86
commit e4fd6c3849
29 changed files with 1995 additions and 46 deletions
+8 -15
View File
@@ -247,23 +247,16 @@ def _build_deduped(
new_why = "Unverified: " + new_why
try:
result.append(Finding(
finding_id=canon.finding_id,
designator=canon.designator,
mpn=canon.mpn,
aspect=canon.aspect,
finding=str(group.get("finding") or canon.finding),
why=new_why,
source_page=group.get("source_page", canon.source_page),
source_quote=canon.source_quote,
source_designator=canon.source_designator,
status=final_status,
recommendation=str(
result.append(canon.model_copy(update={
"finding": str(group.get("finding") or canon.finding),
"why": new_why,
"source_page": group.get("source_page", canon.source_page),
"status": final_status,
"recommendation": str(
group.get("recommendation") or canon.recommendation
),
reference=str(group.get("reference") or canon.reference),
source=canon.source,
))
"reference": str(group.get("reference") or canon.reference),
}))
except Exception:
log.exception("dedupe: failed to build merged Finding")
return None
+9 -14
View File
@@ -394,20 +394,15 @@ def _build_normalized(
new_why = "Unverified: " + new_why
try:
result.append(Finding(
finding_id=canon.finding_id,
designator=canon.designator,
mpn=canon.mpn,
aspect=canon.aspect,
finding=str(entry.get("finding") or canon.finding),
why=new_why,
source_page=entry.get("source_page", canon.source_page),
source_quote=str(entry.get("source_quote") or canon.source_quote),
source_designator=canon.source_designator,
status=final_status,
recommendation=str(entry.get("recommendation") or canon.recommendation),
reference=str(entry.get("reference") or canon.reference),
))
result.append(canon.model_copy(update={
"finding": str(entry.get("finding") or canon.finding),
"why": new_why,
"source_page": entry.get("source_page", canon.source_page),
"source_quote": str(entry.get("source_quote") or canon.source_quote),
"status": final_status,
"recommendation": str(entry.get("recommendation") or canon.recommendation),
"reference": str(entry.get("reference") or canon.reference),
}))
except Exception:
log.exception("normalize: failed to build merged Finding")
return None
+21
View File
@@ -1496,6 +1496,25 @@ async def _stage_passive_extraction(ctx: PipelineContext) -> None:
{"stage": "passive_extraction", "status": "complete"})
def _write_layout_graph(ws: PipelineWorkspace, project_id: str) -> None:
"""Parse optional `.kicad_pcb`. Fail-soft — schema validation must still run."""
pcb = ws.local_path("uploads/pcb.kicad_pcb")
if not pcb.is_file():
return
try:
from backend.pinscopex.parsers_kicad_pcb import parse_kicad_pcb
layout = parse_kicad_pcb(pcb)
out = ws.local_path("layout_graph.json")
out.write_text(layout.model_dump_json(indent=2) + "\n")
logger.info(
"layout_graph: %s footprints, %s nets for %s",
len(layout.footprints), len(layout.nets), project_id,
)
except Exception:
logger.exception("kicad_pcb parse failed — continuing without layout")
async def _stage_graph_build(ctx: PipelineContext) -> None:
"""Stage 4 — Build the design graph from netlist, BOM, and extracted data."""
broker.publish(ctx.project_id, "step_update",
@@ -1525,6 +1544,7 @@ async def _stage_graph_build(ctx: PipelineContext) -> None:
graph_path = ctx.ws.local_path("design_graph.json")
graph_path.write_text(ctx.graph.model_dump_json(indent=2) + "\n")
_write_layout_graph(ctx.ws, ctx.project_id)
broker.publish(ctx.project_id, "step_update",
{"stage": "graph_build", "status": "complete",
@@ -2088,6 +2108,7 @@ async def run_regen_pipeline(
graph_path = ws.local_path("design_graph.json")
graph_path.write_text(graph.model_dump_json(indent=2) + "\n")
_write_layout_graph(ws, project_id)
broker.publish(project_id, "step_update",
{"stage": "graph_build", "status": "complete",
+11
View File
@@ -5,6 +5,7 @@ Each project lives at users/{user_id}/projects/{id}/ with:
uploads/bom.csv — uploaded BOM
uploads/netlist.asc — uploaded netlist
uploads/datasheets/*.pdf — uploaded datasheets
uploads/pcb.kicad_pcb — optional KiCad board (layout checks)
extracted/ — IC extraction output
patterns/ — passive patterns
models/ — cached component specs
@@ -82,6 +83,7 @@ class ProjectMeta(BaseModel):
updated: str = ""
has_bom: bool = False
has_netlist: bool = False
has_pcb: bool = False
# "pads" | "edif" | None — None for legacy projects (pre-EDIF-support).
# Legacy reads fall back to looking for netlist.asc on disk.
netlist_format: str | None = None
@@ -658,6 +660,15 @@ def save_netlist(
return key
def save_pcb(
storage: StorageBackend, user_id: str, project_id: str, data: bytes
) -> str:
key = f"{_project_prefix(user_id, project_id)}/uploads/pcb.kicad_pcb"
storage.write_bytes(key, data)
update_project(storage, user_id, project_id, has_pcb=True)
return key
def save_datasheet(
storage: StorageBackend, user_id: str, project_id: str, mpn: str, data: bytes
) -> str:
+4
View File
@@ -49,6 +49,7 @@ from backend.pinscopex.passive_rail_check import (
check_reset_pullups,
check_supply_decoupling,
)
from backend.pinscopex.bom_match_check import check_bom_schematic_match
TRACE_VERSION = 1
@@ -70,6 +71,9 @@ def _run_deterministic_checks(
("supply_decoupling_check", lambda: check_supply_decoupling(graph, constraints_map)),
("i2c_pullup_check", lambda: check_i2c_pullups(graph, constraints_map)),
("reset_pullup_check", lambda: check_reset_pullups(graph, constraints_map)),
("bom_match_check", lambda: check_bom_schematic_match(
graph.schematic_fields, graph.bom_fields,
)),
):
try:
out.extend(fn())