KB Wiring-Diagram Extraction — Implementation Plan

For agentic workers: execute task-by-task via subagent-driven development. Each task ends with a runnable stdlib unittest check (or a stated live verification). NO git commits, NO code comments (house rules). Steps use - [ ].

Goal: Extract searchable labels/text + best-effort confidence-flagged connection data from the KB wiring diagrams, on existing scans, hybrid CV + vision.

Architecture: A wiring-extract/ module on telep-mainframe. Classical CV (cvtrace.py, run under ocr/venv which has cv2) proposes connectivity; a vision subagent (this session’s model) reads the scan + legend + CV output and emits the fused netlist + searchable text; a small chunker.py patch (kb-vectors) makes the searchable sidecar index alongside the real page.

Design spec: 2026-07-29-kb-wiring-extraction-design. Related: 2026-07-24-kb-vectorize-complete, telep-mainframe.

Global Constraints

  • Work on telep-mainframe as levander; /usr/bin/ssh only. No git commit. No code comments. One decl per line; no inline compound statements.
  • CV code runs under /home/levander/ocr/venv (has cv2 4.11.0, numpy, PIL; ADD scikit-image — do NOT install opencv-headless, it conflicts with the full cv2).
  • Pure-python module code + tests: stdlib unittest. kb-vectors tests run under /home/levander/kb-vectors/venv; module tests under ocr/venv (needs cv2/skimage).
  • PSU brown-out limit: CV/heavy jobs OMP_NUM_THREADS=4 + nice, ONE heavy job at a time; batch offline; no always-on service.
  • NON-DESTRUCTIVE: never modify the source scans or the OCR’d .md; never touch clusters.json. New files only (module dir, per-page sidecars, a chunker patch).
  • Vision + fusion is subagent-driven (session model; box claude is weekly-limited). CV + emit + chunker patch + reindex are box code.
  • Module dir: /home/levander/wiring-extract/. Docs live at /home/levander/knowledgebase/manuals-src/docs/. Vectorizer at /home/levander/kb-vectors/ (chunker.py, index.py; Qdrant manuals @127.0.0.1:6333; index_manual = delete-by-manual_id + upsert).
  • page_url = folder + "/" + manual + "/" + page + "/" (page = md filename w/o .md). Images are sibling _page_N_*.jpeg.

Task 0: CV path-tracing spike (GO/NO-GO gate)

Purpose: validate that classical CV adds real connectivity signal on these 1632px scans BEFORE building the full module. If it doesn’t, fall back to vision-only connections (skip Task 3) and record that.

Files:

  • Create: /home/levander/wiring-extract/spike_cvtrace.py
  • Uses: ocr/venv + scikit-image

Steps:

  • /home/levander/ocr/venv/bin/pip install scikit-image ; verify ocr/venv/bin/python -c "import cv2, skimage, numpy; print(cv2.__version__, skimage.__version__)".
  • Copy the pilot page full-res: cp /home/levander/knowledgebase/manuals-src/docs/suzuki-vitara/wiring-diagrams/_page_14_Figure_0.jpeg /home/levander/wiring-extract/.
  • Write spike_cvtrace.py: crop the top-left CRANKING region (the pilot’s hand-verified sub-circuit), then: grayscale → Otsu/adaptive threshold → small-blob despeckle → skimage.morphology.skeletonize → build a pixel graph (endpoints = 1 neighbor, junctions = ≥3) → count nodes/edges/segments; save a debug overlay PNG (skeleton on the crop).
  • Run it (OMP_NUM_THREADS=4 nice ocr/venv/bin/python spike_cvtrace.py). Copy the debug overlay back and LOOK at it.
  • GATE decision (record in the plan/handoff): does the skeleton follow the actual conductors and yield a plausible node/segment graph matching the pilot cranking topology (battery→fuse→ign switch→starter, ~7 nodes)?
    • PASS → proceed to Task 3 (full cvtrace).
    • FAIL (skeleton is noise / lines broken/merged beyond use at this res) → SKIP Task 3; connections become vision-only in Task 4; note it. Do not build dead CV complexity.

Task 1: Legend extraction → machine-readable legend.json

Files:

  • Create: /home/levander/wiring-extract/legend.json (data), /home/levander/wiring-extract/legend.py (loader), test_legend.py

Interfaces produced: legend.load(manual_family) -> {colors: {code:name}, symbols: {...}, connector_rules: str, howto: str}.

Steps:

  • Vision subagent reads the legend sources for the Vitara family: 013-wire-color-symbols.md + its image _page_8_Picture_7.jpeg (the color table), 011-symbols-and-marks.md, 012-abbreviations.md, 014-how-to-read-wiring-diagram.md, 015-indication-of-connectors-and-how-to-read-them.md. Produce legend.json: colors = full base/stripe abbreviation→name map (B→Black, W→White, R→Red, G→Green, Y→Yellow, Bl→Blue, Lg→Light green, Br→Brown, O→Orange, P→Pink, Gr→Gray, V→Violet, Lbl→Light blue, etc. — read from the image, don’t guess), symbols, connector_rules (how connector/terminal numbers read), howto (flow top→down from fuse/ign to ground).
  • Write legend.py load() reading legend.json; decode_color("B/Y") -> "Black/Yellow" (split on /, map each token, unknown → keep code + mark).
  • test_legend.py: decode_color handles single + base/stripe + unknown tokens; legend.json parses and has ≥12 colors. Run under any venv (pure). PASS.

Task 2: diagram_index.py — find diagram pages + attach legend

Files: Create /home/levander/wiring-extract/diagram_index.py, test_diagram_index.py

Interfaces produced: list_diagram_pages(manual_dir, folder, manual) -> [{page, page_url, md_path, images:[abs...], legend_family}].

Steps:

  • Implement: walk a manual dir, for each .md collect image refs via r"!\[\]\(([^)]+)\)", resolve to sibling abs image paths, form page_url = folder+"/"+manual+"/"+page+"/", tag legend_family="suzuki-vitara" (Vitara/Sidekick share conventions). Filter to pages that HAVE images (diagram pages).
  • test_diagram_index.py: on a temp fixture dir with a md referencing 2 images, returns one page with 2 resolved image paths + correct page_url; a text-only md is excluded. PASS.

Task 3: cvtrace.py — CV connectivity (ONLY if Task 0 gate PASSED)

Files: Create /home/levander/wiring-extract/cvtrace.py, test_cvtrace.py. Runs under ocr/venv.

Interfaces produced: trace(image_path, suppress_boxes=None) -> {nodes:[{id,x,y,kind}], edges:[{a,b,confidence}], nets:[[node_ids]]}.

Steps:

  • Implement the pipeline from the spec: preprocess (gray→threshold→despeckle) → optional text/symbol suppression (mask out suppress_boxes from vision component boxes) → skeletonize → graph (endpoints/junctions) → segment trace → crossing-vs-junction classification (solder-dot blob at 4-way meeting = connect; else hop) → union-find nets. Confidence per edge from trace cleanliness (segment continuity, blob clarity).
  • test_cvtrace.py: build a SYNTHETIC binary schematic (numpy array) with two straight nets, one dot-junction (connect), one hop-crossing (no connect); assert net count = expected, the dot merges nets, the hop does not, endpoints detected. PASS under ocr/venv.
  • Real check: trace the pilot cranking crop; report nodes/edges/nets vs the hand-verified topology (recall + crossing errors) in the task notes.

Task 4: Vision+fusion (subagent) + emit.py

Files: Create /home/levander/wiring-extract/emit.py, test_emit.py. Per-diagram outputs written under the module’s out/ and the sidecar next to the page.

Interfaces produced: emit.write(page, record) → writes out/<manual>__<page>.wiring.json (full netlist+labels+meta+image ref+confidence), and <page>.wires.txt sidecar in the manual dir (searchable text: labels + decoded colors + connection summary), and out/<manual>__<page>.lines (simple documented connection-line format: <from.pin> -- <color> --> <to.pin> [conf]).

Steps:

  • Define the record schema + emit.write (pure IO). test_emit.py: given a sample record, asserts the 3 files’ content shape (json fields present; sidecar contains labels + colors; lines format correct). PASS.
  • Per diagram (subagent-driven, one page at a time, serialized): a vision subagent reads the scan (crop+upscale for fine text) + legend.json + (if Task 3 passed) the cvtrace net-graph JSON, and produces the fused record: components, terminals, labels/systems (searchable), connections {from,to,wire_color:decoded,net_id,confidence} — CV-confirmed+vision-read = high; weak input downgrades; NEVER fabricate untraceable connections. Then call emit.write.
  • Provenance: every record marked AI-extracted; sidecar text prefixed with an AI-extracted note; scan path referenced as source of truth.

Task 5: chunker.py sidecar patch (kb-vectors) — index the sidecar onto the real page

Files: Modify /home/levander/kb-vectors/chunker.py (chunk_manual); update test_chunker.py. Back up chunker.py.bak-wiring first.

Steps:

  • In chunk_manual, after reading a page’s .md text, if a sibling {page}.wires.txt exists, append its content to text under a delimiter (e.g. \n\n[DIAGRAM DATA]\n + sidecar) BEFORE chunking, so the diagram labels/colors ride on the SAME page’s points with the SAME page_url/images. Do not create separate pages. Keep the existing .md-only page discovery (sidecars are not standalone pages).
  • Update test_chunker.py: a page with a sibling .wires.txt → its chunks include the sidecar text and keep the page’s page_url; a page without one is unchanged; test_ignores_non_md still holds (sidecars are not indexed as their own pages). Run kb-vectors/venv/bin/python -m unittest test_chunker. PASS.

Task 6: Batch run (Phase 1: Vitara) + reindex + search-win verification

Files: Create /home/levander/wiring-extract/run.py (batch CLI: run <folder> <manual>), reindex helper.

Steps:

  • run.py: for each diagram page (via diagram_index), run cvtrace (if enabled) → dispatch the vision+fusion → emit sidecars/json. Serialized, thread-capped, idempotent per page. Log stats (pages, connections high/med/low, unreadable regions) — no silent caps.
  • Run Phase 1 on suzuki-vitara/wiring-diagrams (21 pages). Spot-check the cranking netlist JSON against the scan; confirm confidence flags are honest.
  • Re-index just that manual: kb-vectors/venv/bin/python -c "import index; index.index_manual('suzuki-vitara','wiring-diagrams')" (delete-by-manual_id + upsert; safe/incremental).
  • Search-win check: query a term only present in a diagram (e.g. “IC regulator”, “fuel cut controller”) via the existing search path; confirm the wiring page now returns. Report before/after.
  • No regression: kb-vectors + knowledgebase unittest suites pass; source md + scans byte-unchanged; clusters.json untouched.

Self-review

  • Spec coverage: labels-searchable (T1,T4,T5,T6), best-effort connections (T0,T3,T4), hybrid CV+vision (T0/T3 CV, T4 vision+fusion), sidecar-vectorized (T5), trust/confidence (T4 provenance+confidence), Vitara-first (T6). Covered.
  • Deviations from spec (recon-driven): (a) WireViz jot round-trip DESCOPED — project absent on box; emit JSON + simple .lines format instead. (b) Added T1 legend extraction (color table only exists as an image). (c) CV runs under ocr/venv; sidecar needs the T5 chunker patch (indexer is .md-only).
  • Ordering de-risks: T0 spike gates the whole CV investment; vision-only fallback preserved.
  • Type consistency: legend.decode_color, cvtrace.trace, emit.write names stable across tasks; record schema defined in T4 and consumed by T5/T6.