{"record":{"id":"732daea93af8c90c","repo":"zeroclaw-labs/zeroclaw","slug":"tag-contains-a-comma-which-is-used-as-the-ta","errorCode":null,"errorMessage":"tag '{}' contains a comma, which is used as the tag separator","messagePattern":"tag '(.+?)' contains a comma, which is used as the tag separator","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-memory/src/knowledge_graph.rs","lineNumber":224,"sourceCode":"        tags: &[String],\n        source_project: Option<&str>,\n    ) -> anyhow::Result<String> {\n        let conn = self.conn.lock();\n\n        // Enforce max_nodes limit.\n        let count: usize = conn.query_row(\"SELECT COUNT(*) FROM nodes\", [], |r| r.get(0))?;\n        if count >= self.max_nodes {\n            anyhow::bail!(\n                \"knowledge graph node limit reached ({}/{})\",\n                count,\n                self.max_nodes\n            );\n        }\n\n        // Reject tags containing commas since comma is the separator in storage.\n        for tag in tags {\n            if tag.contains(',') {\n                anyhow::bail!(\n                    \"tag '{}' contains a comma, which is used as the tag separator\",\n                    tag\n                );\n            }\n        }\n\n        let id = Uuid::new_v4().to_string();\n        let now = Utc::now().to_rfc3339();\n        let tags_str = tags.join(\",\");\n\n        conn.execute(\n            \"INSERT INTO nodes (id, node_type, title, content, tags, created_at, updated_at, source_project)\n             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)\",\n            params![\n                id,\n                node_type.as_str(),\n                title,\n                content,","sourceCodeStart":206,"sourceCodeEnd":242,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-memory/src/knowledge_graph.rs#L206-L242","documentation":"KnowledgeGraph stores a node's tags as a single comma-joined TEXT column (tags.join(\",\") at insert; split back on ',') and mirrors that string into the FTS index, so the comma is the reserved separator. add_node rejects any tag containing a comma before writing, keeping stored tags unambiguously splittable; without this check a tag like \"rust, memory\" would silently become two tags on read-back.","triggerScenarios":"Calling add_node (or a capture path feeding it) with a tags slice where any element contains ',', e.g. tags = &[\"rust, memory\".to_string()] or user-supplied tag strings pasted from a comma-separated list without splitting.","commonSituations":"Accepting user-provided tags verbatim (\"tags: a, b, c\" typed as one string); forwarding CSV data or LLM-extracted tag lists straight into add_node; test fixtures that join tags themselves before passing them.","solutions":["Split comma-separated input into separate tags before calling add_node: \"a, b\".split(',').map(str::trim).filter(|s| !s.is_empty()).","If a comma is genuinely part of the tag's meaning, replace it with another separator (e.g. '-' or '_') since the storage format cannot encode it.","Validate/sanitize tags at the boundary where user or LLM text enters the system, not at the graph call site only.","Keep the failing tag from the message — it names exactly which element to fix."],"exampleFix":"// before\nlet tags = vec![\"rust, memory\".to_string()];\ngraph.add_node(NodeType::Pattern, \"t\", \"c\", &tags, None)?; // tag contains a comma\n\n// after\nlet tags: Vec<String> = \"rust, memory\"\n    .split(',')\n    .map(str::trim)\n    .filter(|s| !s.is_empty())\n    .map(str::to_string)\n    .collect(); // [\"rust\", \"memory\"]\ngraph.add_node(NodeType::Pattern, \"t\", \"c\", &tags, None)?;","handlingStrategy":"validation","validationCode":"// Sanitize tags at the boundary\nfn clean_tags(raw: &str) -> Vec<String> {\n    raw.split(',')\n        .map(str::trim)\n        .filter(|s| !s.is_empty() && !s.contains(','))\n        .map(str::to_string)\n        .collect()\n}\nlet tags = clean_tags(&user_tags);\nassert!(tags.iter().all(|t| !t.contains(',')));\ngraph.add_node(NodeType::Lesson, title, content, &tags, None)?;","typeGuard":"pub fn tags_are_graph_safe(tags: &[String]) -> bool {\n    tags.iter().all(|t| !t.is_empty() && !t.contains(','))\n}","tryCatchPattern":"// Repair per-tag instead of failing the whole capture\nfor tag in tags.iter().filter(|t| t.contains(',')) {\n    tracing::warn!(tag, \"tag contains reserved separator ','; splitting\");\n}\nlet tags: Vec<String> = tags.iter().flat_map(|t| t.split(',')).map(str::trim).filter(|s| !s.is_empty()).map(str::to_string).collect();\ngraph.add_node(node_type, title, content, &tags, None)?;","preventionTips":["Treat the comma as reserved: split CSV-ish user input into a Vec<String> of single tags before add_node.","Sanitize at the text-ingestion boundary (user input, LLM output), not only at the graph call site.","Include tags_are_graph_safe in test fixtures for any code path that constructs tags dynamically.","If a tag genuinely needs an internal separator, choose '-' or '_' — the storage format cannot encode commas."],"tags":["rust","zeroclaw","knowledge-graph","tags","input-validation","delimiter"],"backgroundTag":"invalid-input-format","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}