`NodeRecord` grew two fields so LLM clients can tell when a node's
colour follows a variable instead of a literal. Without this, the
client can only see the resolved hex — it doesn't know whether to
bump the variable (theme-wide ripple) or write a per-node override.
`crates/openpencil-shell-core/src/mcp/tools.rs`:
- `NodeRecord { fill_ref: String, stroke_ref: String }`. Empty
string when the node doesn't use a variable for that paint
channel. Empty (not omitted) so clients can probe with a
single `out.get("fill_ref") == Some(&"")` instead of having
to handle the missing-key case as well.
- `get_node_snapshot` + `walk_node` thread the document's
`var_table` through the tree walk and look up
`var_table.fill_refs[node.id]` + `var_table.stroke_refs[
node.id]` per node. The lookup is O(log n) BTreeMap, so the
full-document walk stays O(n log n) — fine for typical
documents (≤ thousands of nodes); LLM call frequency is far
below paint frequency anyway.
- The `get_node` `call()` payload adds the two new keys
alongside the existing kind / name / bounds / parent_id.
Test (1 added, 281 shell-core total, 33 mcp module total):
- `get_node_surfaces_fill_and_stroke_refs` — installs
`fill_refs[11] = "color-primary"` +
`stroke_refs[11] = "color-accent"` on the sample doc, calls
`get_node` with node_id=11, asserts the payload carries the
variable names. A second call with node_id=12 (no ref
mapping) asserts both fields are empty strings (not absent),
pinning the API contract.
The MCP read-side now covers the full graph that paint-time `$ref`
substitution uses: list_variables → choose a variable;
list_pages → choose a page; get_document_info → counts;
get_selection → current focus; get_node(id) → kind, bounds,
parent, and which paint channels follow which variable.