Pinscope open-source core

Agentic schematic validation: datasheet extraction via Claude Console
Skills, netlist/BOM design graph, per-IC direct datasheet review with
page citations, capacitor derating, Next.js report UI.

Extracted from the Pinscope cloud codebase. Auth and billing live in the
private gateway repo behind stable seams (billing_hook.py, adapter files
listed in CLAUDE.md).
This commit is contained in:
Siddharth Kothari
2026-07-16 21:29:45 -07:00
commit 6672d2be57
254 changed files with 56662 additions and 0 deletions
+212
View File
@@ -0,0 +1,212 @@
#!/usr/bin/env python3
"""Migrate library datasheets to content-addressed blob storage.
Converts flat ``library/datasheets/{name}.pdf`` files into the new layout::
library/datasheets/blobs/{md5}.pdf -- unique content
library/datasheets/refs/{name}.json -- MPN pointer
Also updates ``datasheet_key`` in pattern JSON files to point to the new
blob paths.
Works with both LocalStorageBackend and GCSStorageBackend depending on
whether GCS_BUCKET is set.
Usage:
# Dry run (default) -- shows what would happen
python -m scripts.migrate_datasheets_to_blobs
# Create blobs + refs + update patterns
python -m scripts.migrate_datasheets_to_blobs --apply
# After verifying, delete original flat files
python -m scripts.migrate_datasheets_to_blobs --apply --cleanup
"""
from __future__ import annotations
import argparse
import tempfile
from pathlib import Path
from backend.services.datasheet_store import (
BLOB_PREFIX,
REF_PREFIX,
blob_key,
compute_md5_from_path,
)
def get_storage():
from backend.config import settings
if settings.gcs_bucket:
from backend.services.storage_gcs import GCSStorageBackend
return GCSStorageBackend(settings.gcs_bucket)
else:
from backend.services.storage import LocalStorageBackend
return LocalStorageBackend(settings.data_dir)
def main():
parser = argparse.ArgumentParser(
description="Migrate library datasheets to content-addressed blob storage",
)
parser.add_argument(
"--apply", action="store_true",
help="Actually create blobs/refs and update patterns (default is dry run)",
)
parser.add_argument(
"--cleanup", action="store_true",
help="Delete original flat PDF files (only with --apply, after verification)",
)
args = parser.parse_args()
if args.cleanup and not args.apply:
parser.error("--cleanup requires --apply")
storage = get_storage()
print(f"Storage backend: {type(storage).__name__}")
# List all flat library datasheet PDFs (exclude blobs/ and refs/ subdirs)
all_keys = storage.list_recursive("library/datasheets/")
flat_pdfs = [
k for k in all_keys
if k.endswith(".pdf")
and not k.startswith(BLOB_PREFIX)
and not k.startswith(REF_PREFIX)
]
print(f"Found {len(flat_pdfs)} flat library datasheet PDF(s)")
if not flat_pdfs:
print("Nothing to migrate")
return
# Phase 1: Create blobs and refs for each flat PDF
blobs_created = 0
blobs_skipped = 0
refs_created = 0
hash_map: dict[str, str] = {} # old key -> md5 hash
with tempfile.TemporaryDirectory() as tmpdir:
for key in flat_pdfs:
filename = key.rsplit("/", 1)[-1]
name = filename.removesuffix(".pdf")
# Download to temp
local_path = Path(tmpdir) / filename
storage.download_to_local(key, local_path)
md5 = compute_md5_from_path(local_path)
hash_map[key] = md5
bk = blob_key(md5)
rk = f"{REF_PREFIX}{name}.json"
if args.apply:
if not storage.exists(bk):
storage.upload_from_local(local_path, bk)
blobs_created += 1
else:
blobs_skipped += 1
storage.write_json(rk, {"hash": md5, "blob_key": bk})
refs_created += 1
else:
existing = storage.exists(bk)
if existing:
blobs_skipped += 1
print(f" BLOB EXISTS {bk} (hash {md5})")
else:
blobs_created += 1
print(f" WOULD CREATE BLOB {bk}")
refs_created += 1
print(f" WOULD CREATE REF {rk} -> {md5}")
print(f"\nBlobs: {blobs_created} created, {blobs_skipped} already existed")
print(f"Refs: {refs_created} created")
# Deduplicate report
unique_hashes = set(hash_map.values())
if len(flat_pdfs) > len(unique_hashes):
saved = len(flat_pdfs) - len(unique_hashes)
print(f"Deduplication: {len(flat_pdfs)} files -> {len(unique_hashes)} unique blobs ({saved} duplicates)")
# Phase 2: Update pattern datasheet_key values
pattern_keys = storage.list_recursive("library/patterns/")
pattern_jsons = [k for k in pattern_keys if k.endswith(".json")]
patterns_updated = 0
for pk in pattern_jsons:
pat = storage.read_json(pk)
ds_key = pat.get("datasheet_key", "")
# Only update if it matches old flat format
if not ds_key or ds_key.startswith(BLOB_PREFIX):
continue
if not ds_key.startswith("library/datasheets/") or not ds_key.endswith(".pdf"):
continue
md5 = hash_map.get(ds_key)
if md5 is None:
# The pattern references a flat file we didn't find -- skip
print(f" WARNING Pattern {pk} references missing datasheet: {ds_key}")
continue
new_ds_key = blob_key(md5)
if args.apply:
pat["datasheet_key"] = new_ds_key
storage.write_json(pk, pat)
patterns_updated += 1
print(f" UPDATED {pk} datasheet_key -> {new_ds_key}")
else:
patterns_updated += 1
print(f" WOULD UPDATE {pk} datasheet_key: {ds_key} -> {new_ds_key}")
print(f"\nPatterns updated: {patterns_updated}")
# Phase 3: Verify (when applying)
if args.apply:
print("\n--- Verification ---")
errors = 0
# Every ref should point to an existing blob
all_refs = storage.list_recursive(REF_PREFIX)
for rk in all_refs:
if not rk.endswith(".json"):
continue
ref = storage.read_json(rk)
bk = ref.get("blob_key")
if not bk or not storage.exists(bk):
print(f" ERROR Ref {rk} points to missing blob: {bk}")
errors += 1
# Every pattern datasheet_key should resolve
for pk in pattern_jsons:
pat = storage.read_json(pk)
ds_key = pat.get("datasheet_key", "")
if ds_key and ds_key.startswith(BLOB_PREFIX) and not storage.exists(ds_key):
print(f" ERROR Pattern {pk} references missing blob: {ds_key}")
errors += 1
if errors:
print(f"\n{errors} error(s) found -- do NOT run --cleanup until resolved")
else:
print("All refs and patterns verified OK")
# Phase 4: Cleanup old flat files
if args.cleanup:
print("\n--- Cleanup ---")
deleted = 0
for key in flat_pdfs:
storage.delete_key(key)
deleted += 1
print(f" DELETE {key}")
print(f"\nDeleted {deleted} flat file(s)")
elif args.apply:
print(f"\nFlat files preserved. Run with --apply --cleanup to delete {len(flat_pdfs)} original file(s)")
else:
print(f"\nDry run complete. Run with --apply to execute.")
if __name__ == "__main__":
main()