{"record":{"id":"54ca94a321a479d1","repo":"astrid-runtime/astrid","slug":"duplicate-capsule-name","errorCode":null,"errorMessage":"duplicate capsule name '{}'","messagePattern":"duplicate capsule name '(.+?)'","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-cli/src/commands/distro/validate.rs","lineNumber":139,"sourceCode":"    if manifest.capsules.is_empty() {\n        anyhow::bail!(\"distro must contain at least one capsule\");\n    }\n\n    // No duplicate capsule names, and each name must be a valid\n    // identifier. Names become path components in the `.shuttle` layout\n    // (`capsules/<name>.capsule`) and on disk under the capsule store;\n    // constraining them to `^[a-z][a-z0-9-]*$` keeps a manifest from\n    // introducing `/`, `..`, or other path-hostile characters there.\n    let mut seen_names = HashSet::new();\n    for cap in &manifest.capsules {\n        if !is_valid_id(&cap.name) {\n            anyhow::bail!(\n                \"capsule name '{}' is invalid (must match ^[a-z][a-z0-9-]*$)\",\n                cap.name,\n            );\n        }\n        if !seen_names.insert(&cap.name) {\n            anyhow::bail!(\"duplicate capsule name '{}'\", cap.name);\n        }\n        // Distros compose *released* capsules. `branch`/`rev` selectors\n        // can only be honored by compiling from a git ref — the exact\n        // toolchain dependency offline/headless distro seeding exists to\n        // remove. Reject them: a distro must pin a released `version` or\n        // `tag`. (Git-ref installs remain available via the standalone\n        // `astrid capsule install` command.)\n        if cap.branch.is_some() || cap.rev.is_some() {\n            anyhow::bail!(\n                \"capsule '{}': branch/rev require building from source and are not allowed in a \\\n                 distro manifest — pin a released `version` or `tag` (git-ref installs are \\\n                 available via `astrid capsule install`).\",\n                cap.name,\n            );\n        }\n        let version = cap.version.trim();\n        if cap.version != version {\n            anyhow::bail!(","sourceCodeStart":121,"sourceCodeEnd":157,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-cli/src/commands/distro/validate.rs#L121-L157","documentation":"Capsule names are used as unique keys (path components under the capsule store), so validate_manifest tracks names in a HashSet and bails on the second occurrence of the same name. Duplicate names would make capsule file placement and resolution ambiguous.","triggerScenarios":"A manifest with two or more `[[capsules]]` sections sharing the same `name` value.","commonSituations":"Copy-pasting a capsule block to add another version and forgetting to change the name; merging manifests from two sources that both include the same capsule.","solutions":["Rename one of the duplicate capsules (must still match `^[a-z][a-z0-9-]*$`).","Remove the redundant duplicate entry if it was accidental.","Deduplicate programmatically when generating/merging manifests (e.g. last-one-wins or explicit merge policy)."],"exampleFix":"# before\n[[capsules]]\nname = \"http\"\nversion = \"1.0.0\"\n[[capsules]]\nname = \"http\"\nversion = \"1.1.0\"\n\n# after\n[[capsules]]\nname = \"http\"\nversion = \"1.1.0\"","handlingStrategy":"validation","validationCode":"let mut seen = std::collections::HashSet::new();\nfor cap in &manifest.capsules {\n    if !seen.insert(&cap.name) {\n        panic!(\"duplicate capsule name: {}\", cap.name);\n    }\n}","typeGuard":null,"tryCatchPattern":"if let Err(e) = validate_manifest(&manifest) {\n    if e.to_string().contains(\"duplicate capsule name\") {\n        eprintln!(\"dedupe capsules: {e}\");\n    }\n}","preventionTips":["When copy-pasting capsule blocks, change the name immediately","Deduplicate after merging manifests from multiple sources","Key capsule entries by name in generator code to enforce uniqueness"],"tags":["cli","manifest","duplicates","validation"],"backgroundTag":"conflicting-config-options","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}