{"record":{"id":"e8747c14d9a52690","repo":"GraphiteEditor/Graphite","slug":"artboard-should-have-a-primary-input","errorCode":null,"errorMessage":"Artboard should have a primary input","messagePattern":"Artboard should have a primary input","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs","lineNumber":206,"sourceCode":"\t\t\t\tid,\n\t\t\t\tlocation,\n\t\t\t\tdimensions,\n\t\t\t\tbackground,\n\t\t\t\tclip,\n\t\t\t} => {\n\t\t\t\tlet mut modify_inputs = ModifyInputsContext::new(network_interface, responses);\n\n\t\t\t\tlet artboard_layer = modify_inputs.create_artboard(id, location, dimensions, background, clip);\n\t\t\t\tnetwork_interface.move_layer_to_stack(artboard_layer, LayerNodeIdentifier::ROOT_PARENT, 0, &[]);\n\n\t\t\t\t// If there is a non artboard feeding into the primary input of the artboard, move it to the secondary input\n\t\t\t\tlet Some(artboard) = network_interface.document_network().nodes.get(&id) else {\n\t\t\t\t\tlog::error!(\"Artboard not created\");\n\t\t\t\t\treturn;\n\t\t\t\t};\n\t\t\t\tlet document_metadata = network_interface.document_metadata();\n\n\t\t\t\tlet primary_input = artboard.inputs.first().expect(\"Artboard should have a primary input\").clone();\n\t\t\t\tif let NodeInput::Node { node_id, .. } = &primary_input {\n\t\t\t\t\tif network_interface.is_artboard(node_id, &[]) {\n\t\t\t\t\t\t// Nothing to do here: we have a stack full of artboards!\n\t\t\t\t\t} else if network_interface.is_layer(node_id, &[]) {\n\t\t\t\t\t\t// We have a stack of non-layer artboards.\n\t\t\t\t\t\tfor (insert_index, layer) in LayerNodeIdentifier::ROOT_PARENT.children(document_metadata).filter(|&layer| layer != artboard_layer).enumerate() {\n\t\t\t\t\t\t\t// Parent the layer to our new artboard (retaining ordering)\n\t\t\t\t\t\t\tresponses.add(NodeGraphMessage::MoveLayerToStack {\n\t\t\t\t\t\t\t\tlayer,\n\t\t\t\t\t\t\t\tparent: artboard_layer,\n\t\t\t\t\t\t\t\tinsert_index,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t// Apply a translation to prevent the content from shifting\n\t\t\t\t\t\t\tresponses.add(GraphOperationMessage::TransformChange {\n\t\t\t\t\t\t\t\tlayer,\n\t\t\t\t\t\t\t\ttransform: DAffine2::from_translation(-location),\n\t\t\t\t\t\t\t\ttransform_in: TransformIn::Local,\n\t\t\t\t\t\t\t\tskip_rerender: true,","sourceCodeStart":188,"sourceCodeEnd":224,"githubUrl":"https://github.com/GraphiteEditor/Graphite/blob/c507b356453361e31638b8bff8f6d46b6da2961e/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs#L188-L224","documentation":"Panics when an `Artboard` node fetched from the document network has an empty `inputs` vec, so `.inputs.first()` returns `None`. The handler (`GraphOperationMessage::CreateArtboard` flow) assumes every artboard node carries at least a primary input; the template registered for `\"Artboard\"` in `DOCUMENT_NODE_TYPES` defines that input. The expect guards against definition drift where the Artboard node template is created or serialized with zero inputs.","triggerScenarios":"Creating an artboard (or artboard-containing document import) triggers `create_artboard` + this handler. The panic fires if the node stored at `id` has no inputs: a hand-edited/corrupted `.graphite` file, a deserialization path that builds an Artboard node with no template inputs, or an Artboard `DocumentNodeDefinition` whose `node_template.inputs` was emptied during a registry refactor.","commonSituations":"Editing the Artboard definition's template and removing/reordering the primary input; loading old documents serialized against a different node schema; programmatic document construction that inserts an Artboard node via a raw template instead of `resolve_network_node_type(\"Artboard\")`.","solutions":["Check the `\"Artboard\"` definition in `document_node_definitions.rs` — its `node_template.inputs` must contain at least the primary `Artboard`-typed input; restore it if a refactor removed it.","If loading user files, validate/repair the deserialized node before this handler runs (reject or re-template artboards with zero inputs).","Replace `expect` with `let-else`: log `\"Artboard has no primary input\"` and return early so one bad node cannot crash the document.","Add a test asserting `resolve_network_node_type(\"Artboard\").node_template.inputs` is non-empty to catch registry drift at CI time."],"exampleFix":"// before\nlet primary_input = artboard.inputs.first().expect(\"Artboard should have a primary input\").clone();\n\n// after\nlet Some(primary_input) = artboard.inputs.first() else {\n    log::error!(\"Artboard node {id} has no primary input; skipping input reshuffle\");\n    return;\n};\nlet primary_input = primary_input.clone();","handlingStrategy":"validation","validationCode":"// Before creating an artboard, verify the definition provides a primary input:\nlet has_primary_input = document_node_definitions::resolve_network_node_type(\"Artboard\")\n    .is_some_and(|def| !def.node_template.inputs.is_empty());\nif !has_primary_input {\n    log::error!(\"Artboard definition lacks a primary input; aborting artboard creation\");\n    return;\n}","typeGuard":"fn node_has_primary_input(node: &DocumentNode) -> bool {\n    !node.inputs.is_empty()\n}","tryCatchPattern":"// Rust has no try/catch; convert the invariant into a checked branch:\nmatch artboard.inputs.first() {\n    Some(primary_input) => { /* existing logic */ }\n    None => log::error!(\"Artboard {} has no primary input\", id),\n}","preventionTips":["Keep a unit test asserting the Artboard template has a non-empty inputs vec","Validate deserialized documents: reject or repair artboard nodes with zero inputs before handlers run","When changing the Artboard definition template, run the artboard-creation integration test","Use let-else instead of expect for any structural assumption about deserialized nodes"],"tags":["rust","panic","expect","artboard","node-inputs","graph-operation","graphite"],"backgroundTag":"missing-node-input","analyzedSha":"c507b356453361e31638b8bff8f6d46b6da2961e","analyzedAt":"2026-08-16T21:57:18.596Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}