{"record":{"id":"f24e65add726fb49","repo":"zeroclaw-labs/zeroclaw","slug":"subgraph-depth-must-be-greater-than-0","errorCode":null,"errorMessage":"subgraph depth must be greater than 0","messagePattern":"subgraph depth must be greater than 0","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-memory/src/knowledge_graph.rs","lineNumber":549,"sourceCode":"        while let Some(row) = rows.next()? {\n            results.push(row_to_node(row)?);\n        }\n        Ok(results)\n    }\n\n    /// Maximum allowed subgraph traversal depth.\n    const MAX_SUBGRAPH_DEPTH: usize = 100;\n\n    /// Extract a subgraph starting from `root_id` up to `depth` hops.\n    /// `depth` must be between 1 and `MAX_SUBGRAPH_DEPTH` (100).\n    /// Uses a recursive CTE for efficient single-query bidirectional traversal.\n    pub fn get_subgraph(\n        &self,\n        root_id: &str,\n        depth: usize,\n    ) -> anyhow::Result<(Vec<KnowledgeNode>, Vec<KnowledgeEdge>)> {\n        if depth == 0 {\n            anyhow::bail!(\"subgraph depth must be greater than 0\");\n        }\n        let depth = depth.min(Self::MAX_SUBGRAPH_DEPTH);\n        let conn = self.conn.lock();\n\n        // Collect reachable node IDs via recursive CTE (bidirectional traversal).\n        let mut node_stmt = conn.prepare(\n            \"WITH RECURSIVE reachable(id, depth) AS (\n                SELECT ?1, 0\n                UNION\n                SELECT CASE WHEN e.from_id = r.id THEN e.to_id ELSE e.from_id END, r.depth + 1\n                FROM reachable r\n                JOIN edges e ON e.from_id = r.id OR e.to_id = r.id\n                WHERE r.depth < ?2\n             )\n             SELECT DISTINCT n.id, n.node_type, n.title, n.content, n.tags,\n                    n.created_at, n.updated_at, n.source_project\n             FROM reachable rc\n             JOIN nodes n ON n.id = rc.id\",","sourceCodeStart":531,"sourceCodeEnd":567,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-memory/src/knowledge_graph.rs#L531-L567","documentation":"get_subgraph(root_id, depth) walks the graph outward from a root via a recursive CTE and treats depth as 'how many hops to include', so 0 would select nothing. It validates depth > 0 up front and bails with this message; after validation the depth is internally clamped to MAX_SUBGRAPH_DEPTH (100), so 0 is the only invalid value — anything from 1 upward is accepted.","triggerScenarios":"Calling get_subgraph with depth == 0, most often because a caller-computed depth (levels minus one, a remaining-depth variable in a loop, or a config default of 0) underflowed or was never set. Hit directly or via client-facing wrappers that forward a user-supplied depth (client_relationship_types_roundtrip_through_queries tests exercise the same path).","commonSituations":"UI code mapping 'current level' to hops as level-1 and rendering level 0; pagination/expansion loops that reach 0 remaining hops and call instead of stopping; configs defaulting depth to 0 meaning 'unlimited' (here unlimited is expressed by any value >= 100 via the clamp, not 0).","solutions":["Pass at least 1: depth 1 means 'the root plus its direct neighbors'.","If the value is computed, clamp it before the call: let depth = depth.clamp(1, KnowledgeGraph::MAX_SUBGRAPH_DEPTH).","If 0 was meant as 'no limit', pass 100 (MAX_SUBGRAPH_DEPTH) instead — the implementation clamps to exactly that.","Guard loops that decrement depth so they stop at 1 rather than calling with 0."],"exampleFix":"// before\nlet (nodes, edges) = graph.get_subgraph(&root, remaining_depth)?; // remaining_depth == 0 -> bail\n\n// after\nlet depth = remaining_depth.max(1); // 0 hops is meaningless; 1 = root + direct neighbors\nlet (nodes, edges) = graph.get_subgraph(&root, depth)?;","handlingStrategy":"validation","validationCode":"// Clamp computed depths before the call\nlet depth = depth.clamp(1, 100); // 1..=KnowledgeGraph::MAX_SUBGRAPH_DEPTH\nlet (nodes, edges) = graph.get_subgraph(&root_id, depth)?;","typeGuard":"pub fn valid_subgraph_depth(depth: usize) -> bool {\n    depth >= 1 // values above 100 are clamped internally, only 0 is invalid\n}","tryCatchPattern":"// Expansion loops: treat 0 as 'direct neighbors only'\nlet depth = if remaining == 0 { 1 } else { remaining };\nmatch graph.get_subgraph(&root_id, depth) {\n    Ok(sub) => render(sub),\n    Err(e) if e.to_string().contains(\"depth must be greater than 0\") => {\n        render(graph.get_subgraph(&root_id, 1)?)\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Map UI levels to hops explicitly (level N -> depth N, never N-1).","Stop decrementing loops at 1; 0 remaining hops means 'stop', not 'query with 0'.","Express 'unlimited' as MAX_SUBGRAPH_DEPTH (100), not 0 — the implementation clamps down to 100 from above.","Assert depth >= 1 in debug builds where the value is computed."],"tags":["rust","zeroclaw","knowledge-graph","subgraph","argument-validation","off-by-one"],"backgroundTag":"invalid-argument-value","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}