---
name: lvkit-resolve-primitive
description: Resolve a single unknown LabVIEW primitive by following a strict verification process against documentation and graph context. Called when PrimitiveResolutionNeeded fires during lvkit generate.
allowed-tools: Bash, Read, Write, Edit, Glob, Grep, WebSearch, WebFetch
---

# Resolve Unknown Primitive

When `PrimitiveResolutionNeeded` fires for an unknown primitive during `lvkit generate`, this skill resolves it by identifying the function and writing a JSON mapping. Follow ALL steps IN ORDER. Do NOT skip. Do NOT guess.

(`TerminalResolutionNeeded` is a separate exception for known primitives where a specific terminal index doesn't match — that's a different problem with the same workflow.)

## Step 0: Detect mode

This skill runs in two contexts. The destination directory and the cross-reference corpus differ. Decide which one applies BEFORE you do anything else.

Read `pyproject.toml` from the current directory (walk up if needed). If it contains `name = "lvkit"`, you are working **inside lvkit itself**:

- Destination: `data/primitives.json` (lvkit's shipped, cleanroom data)
- Cross-reference corpus: lvkit's `.lvkit/cache/samples/` directory of real VIs
- Mark `"verified": true` only after multi-instance cross-check
- The mapping must be cleanroom — derived from public documentation, NOT from licensed LabVIEW source

Otherwise, you are working **inside a downstream user's project**:

- Destination: `.lvkit/primitives.json` (project-local store; run `lvkit init` first if `.lvkit/` doesn't exist)
- Cross-reference corpus: the user's own VI tree. The diagnostic includes the **qualified VI name** of the caller — use it to find the calling VI on disk.
- Do NOT cross-reference lvkit's `.lvkit/cache/samples/` — the user's project doesn't have them
- The mapping you write may be derived from licensed sources; that's the user's call. lvkit itself never reads `.lvkit/`.

In **both** modes, the function-identification step (Step 5) uses live web search against NI's online documentation. lvkit does not bundle the LabVIEW reference manual.

## Step 1: Record the diagnostic

Write down the EXACT diagnostic output:
- primResID
- Every terminal: index, direction, type
- The VI name (and the qualified name if present — "In VI: ..." line)

**IMPORTANT: the heap carries the primitive's FULL connector pane — identify by it, not just wired terminals.** LabVIEW serializes EVERY terminal of a primitive in its `termList`, including UNWIRED ones, each with its own index, direction (from `objFlags`), and declared type (from its own `typeDesc`). So the full terminal count + per-terminal type signature ARE available in the parsed data (verified in-tree: prim 1302 carries an unwired output typed `NumUInt32`; prim 1056 an unwired output typed `NumFloat64`). This full connector-pane signature is the strongest discriminator — use it.

**The `PrimitiveResolutionNeeded` diagnostic already lists this full pane.** It reports EVERY terminal — each marked `wired` or `UNWIRED` — with its index, direction, and declared type (the graph carries unwired terminals; the diagnostic does not drop them). So identify from the whole signature shown in the diagnostic: total terminal count + each terminal's direction + type. Do NOT match only the wired subset, and do NOT assume the count is short.

For a **polymorphic/adaptive** primitive, an UNWIRED terminal may show its adapt/placeholder type rather than a concrete one — count, direction, and position stay reliable; concrete types come from the wired terminals.

## Step 2: Get more instances of this primResID — grep the XML, DON'T parse the corpus

**NEVER parse every VI in the corpus to find instances.** A VI tree can be thousands of
VIs / ~1 GB of XML; `rglob('*.vi')` + `parse_vi` on all of them loads everything into memory at
once and can exhaust RAM and CRASH the machine (WSL especially). Instead: **grep the already-dumped
block-diagram XML** for the primResID, then parse ONLY the matched VIs — and only those VIs,
**not their subVIs**.

The primResID is serialized verbatim as `<primResID>PRIM_ID</primResID>` in each VI's `*_BDHb.xml`.

**1. Ensure the block-diagram XML is dumped (pylabview — no LabVIEW license needed).** If your
`.vi` files have no `*_BDHb.xml` beside them yet, extract each one — ONE subprocess per VI, so
memory stays flat:
```bash
# <root> = your project root
python3 -c "
from pathlib import Path
from lvkit.extractor import extract_vi_xml
for vi in Path('<root>').rglob('*.vi'):
    try: extract_vi_xml(str(vi))   # writes *_BDHb.xml beside the VI (cached); no subVIs
    except Exception as e: print('skip', vi.name, e)
"
```

**2. Grep the dumped XML for the primResID (memory-flat, instant):**
```bash
grep -rl "<primResID>PRIM_ID</primResID>" <root> --include=*_BDHb.xml
```
This lists exactly the VIs that contain the primitive — usually a handful.

**3. Parse ONLY those matched VIs — no subVIs, no dependency loading.** Feed the matched
`*_BDHb.xml` paths straight to `parse_vi(bd_xml=...)`, which parses just that ONE VI's block
diagram (it does NOT load subVIs or dependencies). Cap the count and `del` each parse:
```bash
# grep -rlZ (null-delimited) pipes matches to the parser — handles SPACES in paths
# (unquoted $(grep ...) word-splits paths like "File Group 0"). Caps at 10; frees each parse.
grep -rlZ "<primResID>PRIM_ID</primResID>" <root> --include=*_BDHb.xml | python3 -c "
import sys
from lvkit.parser.vi import parse_vi
from lvkit.parser.node_types import PrimitiveNode
PRIM_ID = <PRIM_ID>
paths = [p.decode() for p in sys.stdin.buffer.read().split(b'\0') if p]
for bd in paths[:10]:                             # matched _BDHb.xml paths, capped
    diagram = parse_vi(bd_xml=bd).block_diagram   # this VI ONLY — no subVIs
    for node in diagram.nodes:
        if isinstance(node, PrimitiveNode) and node.prim_res_id == PRIM_ID:
            terms = sorted((ti for ti in diagram.terminal_info.values() if ti.parent_uid == node.uid), key=lambda t: t.index)
            sig = [f'{\"out\" if ti.is_output else \"in\"}:idx{ti.index}:{ti.parsed_type.type_name if ti.parsed_type else \"?\"}' for ti in terms]
            print(f'{bd.split(\"/\")[-1]}: primIdx={node.prim_index} {sig}')
    del diagram
"
```
Grep first, parse only the matches, never `rglob('*.vi')` + parse, and never expand subVIs.

## Step 3: Examine graph context

For each instance, check what operations feed into and consume from this primitive. **Trace beyond immediate neighbors** — follow wires through structure boundaries (tunnels, shift registers), nMux nodes, and into/out of SubVI calls. The name of the VI that CALLS the primitive, and the names of VIs/primitives that consume its outputs, are often the strongest identification signal.

The fastest way to see context is `lvkit describe` on the calling VI:

```bash
lvkit describe "<path-to-calling-vi>" --search-path "<library-path>"
```

Or programmatically — parse ONLY the matched `*_BDHb.xml` from Step 2 (no rglob, no subVIs):

```bash
python3 -c "
from lvkit.parser.vi import parse_vi
from lvkit.parser.node_types import PrimitiveNode
BD = '<matched _BDHb.xml from Step 2>'
PRIM_ID = <PRIM_ID>
bd = parse_vi(bd_xml=BD).block_diagram        # this VI ONLY — subVIs are NOT loaded
for node in bd.nodes:
    if isinstance(node, PrimitiveNode) and node.prim_res_id == PRIM_ID:
        my_terms = {uid for uid, ti in bd.terminal_info.items() if ti.parent_uid == node.uid}
        for w in bd.wires:
            if w.from_term in my_terms or w.to_term in my_terms:
                other_uid = w.to_term if w.from_term in my_terms else w.from_term
                other_ti = bd.terminal_info.get(other_uid)
                if other_ti:
                    other_node = next((n for n in bd.nodes if n.uid == other_ti.parent_uid), None)
                    direction = 'output →' if w.from_term in my_terms else 'input ←'
                    print(f'  {direction} {other_node.name if other_node else other_ti.parent_uid}')
        break
"
```

If immediate neighbors are generic (nMux, structure boundaries, constants), trace further —
staying within THIS VI's XML (do not load subVIs; their name in the caller is enough context):
- What VI contains this primitive? The VI's name and purpose give context.
- What do the connected SubVIs do? Check their names.
- What primitives feed into or consume from this one? Check their primResIDs against known entries.

The connected operations, their names, and the data types flowing through reveal what this primitive does.

## Step 4: Cross-check primResID range

Related LabVIEW primitives share primResID ranges:
- 1044-1064: Array operations
- 1061-1081: Numeric/arithmetic
- 1083-1128: Path/comparison/boolean
- 1140-1170: Type conversion, variant, data manipulation
- 1300-1340: Timing, constants, clusters
- 1419-1435: Path operations
- 1500-1540: String operations
- 1600-1610: Flatten/unflatten
- 1809-1911: Array index/sort/delete
- 1999: Path constant
- 2073-2076: Error handling
- 2401: Merge Errors
- 8003-8083: File I/O
- 8100-8101: VI info
- 8201-8205: Variant operations
- 9000-9114: VI Server, references, scripting

Does the terminal signature (types, count, directions) fit the range?

## Step 5: Identify the function via web search

lvkit does not bundle NI's reference manual. Use the WebSearch tool to look up candidate functions on NI's documentation site.

Search queries that work well:

- `LabVIEW <CANDIDATE FUNCTION NAME> function terminals` — broad lookup
- `site:ni.com/docs <CANDIDATE FUNCTION NAME>` — restrict to NI docs
- `LabVIEW <CATEGORY from Step 4> primitives` — when you only know the category

When you find a candidate page, use WebFetch to read the full Inputs/Outputs section. Confirm:

- The terminal TYPES match the actual types from Step 1
- The terminal NAMES and DIRECTIONS match the documentation
- The diagnostic lists the FULL connector pane (every terminal, each marked wired/UNWIRED, with its type) — the whole signature should match the documentation, not just the wired terminals.
- The CONTEXT (Step 3) makes sense for what the function does

If WebSearch is unavailable or returns nothing useful:

- **lvkit mode**: cross-reference the `.lvkit/cache/samples/` corpus more aggressively (Step 2), and ask the user
- **user-project mode**: ask the user to open the calling VI in LabVIEW and read the primitive's context menu / quick help. The qualified VI path from the diagnostic tells you which file to point them at.

## Step 6: Add the JSON entry

Only after completing steps 1-5. The destination depends on the mode you detected in Step 0:

- **lvkit mode** → `data/primitives.json`
- **user-project mode** → `.lvkit/primitives.json`

Add under the `primitives` key:

```json
"PRIM_ID": {
    "name": "Confirmed Function Name",
    "terminals": [
        {"index": N, "direction": "in", "name": "descriptive_python_name", "type": "actual_type"},
        {"index": N, "direction": "out", "name": "descriptive_python_name", "type": "actual_type"}
    ],
    "python_code": {"output_name": "in_N op in_M"},
    "inline": true,
    "verified": true,
    "pdf_page": PAGE_NUMBER
}
```

Rules:
- Terminal **indices** MUST be confirmed from observed wiring (Steps 2-3), NEVER assumed from documentation order. The documentation lists terminals in a different order than the connector pane indices. Example: Split 1D Array docs list "array, index" but the connector pane has index=2 for the numeric index and index=3 for the array.
- Terminal **directions** MUST be confirmed from observed `is_output` flags in the parser data, not from documentation
- Terminal names MUST be valid Python identifiers (no `x=y?`, no `NaN/Path/Refnum?`)
- Template expressions MUST use `in_N` index references matching the OBSERVED connector pane indices
- Include ALL terminals that appear in the actual VI data, including error clusters
- `python_code` dict keys match output terminal names
- Mark `"verified": true` only if indices confirmed from observed connections
- The parser reports **element types** for array terminals (e.g., NumUInt8 for Array of UInt8). Don't confuse element types with scalar types — check the output terminal types and wiring context

## Step 7: Re-run generation

```bash
lvkit generate "<vi-path>" -o "<output-dir>" --search-path "<library-path>"
```

If the same primitive fails again, the terminal matching is wrong — go back to Step 1.
If a NEW primitive fails, start this process again for that one.

## Placeholder entries (`"placeholder": true`) — LAST RESORT ONLY

If after completing ALL steps 1-5 you **cannot identify the primitive**, you may add a placeholder entry. This is the LAST RESORT — only after:
1. You ran Step 2 and checked all instances across the available corpus
2. You ran Step 3 and traced context beyond immediate neighbors
3. You checked the primResID range and searched documentation thoroughly
4. You asked the user if they recognize the terminal signature

A placeholder allows generation to proceed with a warning instead of crashing:

```json
"PRIM_ID": {
    "name": "Unknown Category Primitive PRIM_ID",
    "placeholder": true,
    "terminals": [...all terminals from the graph...],
    "python_code": "pass"
}
```

Placeholder entries emit a `warnings.warn()` and generate `pass` + a TODO comment. They NEVER silently succeed — they always leave a visible marker in the output.

**Alternative: soft codegen mode.** Instead of authoring a placeholder, you can re-run `lvkit generate --placeholder-on-unresolved`. The generated Python contains an inline `raise PrimitiveResolutionNeeded(...)` statement with the same diagnostic context. You can fix the gap contextually in the Python (or come back and write a real mapping later).

**You MUST run this skill for EVERY unknown primitive.** No exceptions. No skipping steps. No adding placeholders without completing the full investigation first.

## NEVER do these things

- NEVER guess a function name from terminal types alone
- NEVER say "polymorphic" to explain away type mismatches — ask the user
- NEVER copy a name from another entry because "it looks similar"
- NEVER fill python_code without confirming semantics from the documentation
- NEVER assume terminal indices from documentation listing order — always confirm from observed wiring in Steps 2-3
- NEVER assume a primResID maps to a different function based on terminal types — primitive polymorphism must be observed in data or confirmed by the user
- NEVER skip the context check (Step 3) — it reveals what the function actually does
- NEVER batch-fill entries — one at a time, fully verified
- NEVER add a placeholder without completing ALL steps 1-5 first
- NEVER skip running this skill — it is MANDATORY for every unknown primitive
- NEVER write user-mode mappings into lvkit's `data/` (cleanroom contamination)
- NEVER write lvkit-mode mappings into a user project's `.lvkit/` (wrong destination)
