{"record":{"id":"be113472796dfc33","repo":"Hmbown/CodeWhale","slug":"snapshots-carry-one-removal-token-per-rule","errorCode":null,"errorMessage":"snapshots carry one removal token per rule","messagePattern":"snapshots carry one removal token per rule","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/commands/groups/config/permissions.rs","lineNumber":55,"sourceCode":"    }\n    let Ok(display_index) = parts[1].parse::<usize>() else {\n        return usage_error(app);\n    };\n    let Some(index) = display_index.checked_sub(1) else {\n        return rule_not_found(app, display_index);\n    };\n\n    if parts.len() == 2 {\n        let snapshot = match load_snapshot(app) {\n            Ok(snapshot) => snapshot,\n            Err(error) => return operation_error(app, &error),\n        };\n        let Some(rule) = snapshot.rules().get(index) else {\n            return rule_not_found(app, display_index);\n        };\n        let token = snapshot\n            .removal_token(index)\n            .expect(\"snapshots carry one removal token per rule\");\n        let command = format!(\"/permissions remove {display_index} --confirm {token}\");\n        let rule = format_rule(app, display_index, rule);\n        let message = tr(app.ui_locale, MessageId::PermissionsRemovePreview)\n            .replace(\"{index}\", &display_index.to_string())\n            .replace(\"{rule}\", &rule)\n            .replace(\"{command}\", &command);\n        return CommandResult::message(message);\n    }\n\n    if !parts[2].eq_ignore_ascii_case(\"--confirm\") || parts[3].is_empty() {\n        return usage_error(app);\n    }\n    let removed =\n        match codewhale_config::remove_permission_rule(app.config_path.clone(), index, parts[3]) {\n            Ok(rule) => rule,\n            Err(error) => return operation_error(app, &error),\n        };\n    let message = tr(app.ui_locale, MessageId::PermissionsRemoved)","sourceCodeStart":37,"sourceCodeEnd":73,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/commands/groups/config/permissions.rs#L37-L73","documentation":"Panic from `snapshot.removal_token(index)` in `remove_permission`. The code asserts an internal invariant: every rule present in the permission snapshot has a corresponding removal token. If the rules list and token list ever disagree in length, the lookup returns None and the expect panics.","triggerScenarios":"Invoking `/permissions remove <index>` where the snapshot at that index has no removal token — i.e., snapshot rules and removal tokens were built out of sync when the snapshot was constructed.","commonSituations":"A code change added a rule source without extending removal-token generation; concurrent mutation of permissions between snapshot creation and removal command handling.","solutions":["Fix snapshot construction so removal tokens are generated for every rule","Check `snapshot.rules().len()` equals the token count before indexing","Replace the expect with graceful handling returning `rule_not_found` if tokens can legitimately be absent"],"exampleFix":"// before\nlet token = snapshot\n    .removal_token(index)\n    .expect(\"snapshots carry one removal token per rule\");\n// after\nlet Some(token) = snapshot.removal_token(index) else {\n    return rule_not_found(app, display_index);\n};","handlingStrategy":"type-guard","validationCode":"debug_assert_eq!(snapshot.rules().len(), snapshot.removal_token_count(), \"rules and removal tokens out of sync\");","typeGuard":"fn removal_token(snapshot: &Snapshot, index: usize) -> Option<String> { snapshot.removal_token(index) }","tryCatchPattern":"let Some(token) = snapshot.removal_token(index) else { return rule_not_found(app, display_index); };","preventionTips":["Keep token generation in the same constructor as rules so they can't diverge","Add a unit test asserting every rule index has a token","Avoid concurrent mutation of permission state between snapshot and command"],"tags":["rust","panic","invariant","permissions"],"backgroundTag":"internal-invariant-violation","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-22T11:17:16.035Z"}