{"record":{"id":"a1d7df90544e2a8d","repo":"zeroclaw-labs/zeroclaw","slug":"candidate-sop-did-not-validate-as-exactly-one-load","errorCode":null,"errorMessage":"candidate SOP did not validate as exactly one loadable SOP","messagePattern":"candidate SOP did not validate as exactly one loadable SOP","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-runtime/src/sop/procedural_memory.rs","lineNumber":241,"sourceCode":"    Ok(proposal)\n}\n\nfn load_required(engine: &SopEngine, proposal_id: &str) -> Result<ProposalRecord> {\n    engine\n        .load_proposal(proposal_id)\n        .map_err(anyhow::Error::new)?\n        .ok_or_else(|| anyhow::Error::msg(format!(\"proposal not found: {proposal_id}\")))\n}\n\nfn validate_candidate(sop_name: &str, manifest_toml: &str, procedure_markdown: &str) -> Result<()> {\n    let tmp = tempfile::tempdir()?;\n    let sop_dir = tmp.path().join(slugify(sop_name));\n    fs::create_dir_all(&sop_dir)?;\n    fs::write(sop_dir.join(\"SOP.toml\"), manifest_toml)?;\n    fs::write(sop_dir.join(\"SOP.md\"), procedure_markdown)?;\n    let sops = load_sops_from_directory(tmp.path(), super::parse_execution_mode(\"supervised\"));\n    if sops.len() != 1 {\n        bail!(\"candidate SOP did not validate as exactly one loadable SOP\");\n    }\n    if sops[0].name != sop_name {\n        bail!(\n            \"candidate manifest name '{}' does not match proposal target '{}'\",\n            sops[0].name,\n            sop_name\n        );\n    }\n    if sops[0].steps.is_empty() {\n        bail!(\"candidate SOP has no parsed steps\");\n    }\n    Ok(())\n}\n\nfn scan_candidate(manifest_toml: &str, procedure_markdown: &str) -> Option<String> {\n    let detector = LeakDetector::new();\n    let content = format!(\"{manifest_toml}\\n{procedure_markdown}\");\n    match detector.scan(&content) {","sourceCodeStart":223,"sourceCodeEnd":259,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/sop/procedural_memory.rs#L223-L259","documentation":"ZeroClaw's procedural-memory pipeline validates every SOP candidate before it is persisted or applied: validate_candidate writes the proposed SOP.toml and SOP.md into a temp directory (under slugify(sop_name)) and loads it back through the real loader, load_sops_from_directory, in supervised mode. This error means the loader returned something other than exactly one SOP from that directory - almost always zero, because the manifest is invalid TOML or does not satisfy the loader's schema. It fires from create_proposal and apply_proposal.","triggerScenarios":"Calling sop::procedural_memory::create_proposal(engine, draft) or apply_proposal(...) with a draft.manifest_toml (or a stored proposal's manifest) that fails to parse as TOML, is missing the [sop] table or required keys, so load_sops_from_directory returns 0 SOPs; or a directory layout that the loader expands into more than one SOP (sops.len() > 1).","commonSituations":"Hand-written SOP.toml with a typo (unbalanced quote, wrong key such as title instead of name), a manifest copied from an older ZeroClaw version whose loader schema changed, or a Pending proposal persisted before an upgrade and applied after the loader's requirements tightened.","solutions":["Reproduce the failure directly: write the candidate SOP.toml/SOP.md into an empty temp dir and run load_sops_from_directory(tmp, parse_execution_mode(\"supervised\")) - any load error points at the exact manifest problem","Fix the manifest TOML: valid syntax and a [sop] table with the keys the loader requires (name, description, version), plus at least one [[triggers]] entry (see default_manifest_toml for the shape)","If the manifest is auto-generated (draft.manifest_toml None/blank -> default_manifest_toml) and it still fails, check that slugify(sop_name) is non-empty so the loader actually discovers the directory","After a ZeroClaw upgrade changed the SOP format, discard stale Pending proposals and re-run create_proposal instead of applying the old record"],"exampleFix":"// before\nlet manifest = r#\"[sop]\ntitle = \"Deploy check\"\n\"#;\ncreate_proposal(&engine, ProposalDraft {\n    sop_name: \"deploy-check\".into(),\n    description: \"Verify deploy\".into(),\n    manifest_toml: Some(manifest.into()),\n    procedure_markdown: md,\n    source_run_id: None,\n    requested_by: None,\n})?;\n\n// after\nlet manifest = r#\"[sop]\nname = \"deploy-check\"\ndescription = \"Verify deploy\"\nversion = \"0.1.0\"\n\n[[triggers]]\ntype = \"manual\"\n\"#;\ncreate_proposal(&engine, ProposalDraft { /* same fields with fixed manifest */ ..draft });","handlingStrategy":"validation","validationCode":"// Pre-validate a candidate the same way the pipeline does, before persisting a proposal:\nfn candidate_round_trips(sop_name: &str, manifest: &str, md: &str) -> bool {\n    let tmp = tempfile::tempdir().expect(\"tmpdir\");\n    let dir = tmp.path().join(slugify(sop_name));\n    std::fs::create_dir_all(&dir).unwrap();\n    std::fs::write(dir.join(\"SOP.toml\"), manifest).unwrap();\n    std::fs::write(dir.join(\"SOP.md\"), md).unwrap();\n    let sops = load_sops_from_directory(tmp.path(), parse_execution_mode(\"supervised\"));\n    sops.len() == 1 && sops[0].name == sop_name && !sops[0].steps.is_empty()\n}","typeGuard":null,"tryCatchPattern":"match create_proposal(&engine, draft) {\n    Ok(proposal) => { /* store proposal.id for review */ }\n    Err(e) if e.to_string().contains(\"did not validate as exactly one loadable SOP\") => {\n        // manifest itself is unloadable: return it to the author for repair; do not blind-retry\n    }\n    Err(e) => return Err(e.context(\"sop proposal rejected\")),\n}","preventionTips":["Round-trip every generated manifest through load_sops_from_directory in a temp dir before creating the proposal","Generate manifests with default_manifest_toml instead of hand-writing TOML","Re-create proposals after upgrading ZeroClaw rather than applying records persisted under an older loader"],"tags":["sop","toml","validation","procedural-memory","zeroclaw"],"backgroundTag":"manifest-validation-failed","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}