{"record":{"id":"a589c2b73bac9c78","repo":"gitbutlerapp/gitbutler","slug":"candidate-turns-always-have-review-or-branch-evide","errorCode":null,"errorMessage":"candidate turns always have review or branch evidence","messagePattern":"candidate turns always have review or branch evidence","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but-agentlog/src/projection.rs","lineNumber":544,"sourceCode":"    if has_review {\n        reasons.push(ProjectionMatchReason {\n            kind: ProjectionMatchKind::ReviewTarget,\n        });\n    }\n    if has_branch {\n        reasons.push(ProjectionMatchReason {\n            kind: ProjectionMatchKind::BranchTarget,\n        });\n    }\n    reasons\n}\n\nfn evidence_tier(has_review: bool, has_branch: bool) -> ProjectionEvidenceTier {\n    match (has_review, has_branch) {\n        (true, true) => ProjectionEvidenceTier::Supporting,\n        (true, false) => ProjectionEvidenceTier::Direct,\n        (false, true) => ProjectionEvidenceTier::Possible,\n        (false, false) => unreachable!(\"candidate turns always have review or branch evidence\"),\n    }\n}\n\nfn evidence_rank(tier: ProjectionEvidenceTier) -> usize {\n    match tier {\n        ProjectionEvidenceTier::Supporting => 0,\n        ProjectionEvidenceTier::Direct => 1,\n        ProjectionEvidenceTier::Possible => 2,\n    }\n}\n\nfn project_records(\n    request: &ProjectionRequest,\n    session_key: &str,\n    turn_key: &str,\n    records: SessionRecords,\n    saw_truncated_text: &mut bool,\n) -> Vec<ProjectionRecord> {","sourceCodeStart":526,"sourceCodeEnd":562,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-agentlog/src/projection.rs#L526-L562","documentation":"`evidence_tier` in but-agentlog's projection maps `(has_review, has_branch)` to a `ProjectionEvidenceTier`; the `(false, false)` arm is `unreachable!` because upstream candidate collection only keeps turns that already carry review or branch evidence. The panic therefore signals that the candidate filter upstream of this function was bypassed or regressed, letting evidence-free turns through.","triggerScenarios":"A code path builds a candidate-turn list without the evidence filter and feeds it to `evidence_tier`; the upstream filter's condition is widened during a refactor; test fixtures synthesize turns with no review and no branch attached.","commonSituations":"Extending but-agentlog projections with new candidate sources; loosening the candidate query during debugging; fixture data that omits the evidence fields.","solutions":["Filter candidates to `has_review || has_branch` before computing tiers.","If new legitimate turn kinds can be evidence-free, change `evidence_tier` to return `Option<ProjectionEvidenceTier>` and skip them instead of panicking.","Fix fixtures so candidate turns include at least one evidence kind."],"exampleFix":"// before\nfn evidence_tier(has_review: bool, has_branch: bool) -> ProjectionEvidenceTier {\n    match (has_review, has_branch) {\n        // ...\n        (false, false) => unreachable!(\"candidate turns always have review or branch evidence\"),\n    }\n}\n\n// after\nfn evidence_tier(has_review: bool, has_branch: bool) -> Option<ProjectionEvidenceTier> {\n    match (has_review, has_branch) {\n        (true, true) => Some(ProjectionEvidenceTier::Supporting),\n        (true, false) => Some(ProjectionEvidenceTier::Direct),\n        (false, true) => Some(ProjectionEvidenceTier::Possible),\n        (false, false) => None, // not a candidate; skip\n    }\n}","handlingStrategy":"type-guard","validationCode":"// only feed turns that carry evidence into tier computation\nlet candidates: Vec<_> = turns\n    .into_iter()\n    .filter(|t| t.review_id.is_some() || t.branch_name.is_some())\n    .collect();\nfor turn in candidates {\n    let tier = evidence_tier(turn.review_id.is_some(), turn.branch_name.is_some());\n    // ...\n}","typeGuard":"fn evidence_tier_safe(has_review: bool, has_branch: bool) -> Option<ProjectionEvidenceTier> {\n    match (has_review, has_branch) {\n        (true, true) => Some(ProjectionEvidenceTier::Supporting),\n        (true, false) => Some(ProjectionEvidenceTier::Direct),\n        (false, true) => Some(ProjectionEvidenceTier::Possible),\n        (false, false) => None, // not a candidate\n    }\n}","tryCatchPattern":null,"preventionTips":["Keep the upstream candidate filter (review OR branch evidence) and the tier function in the same module review scope.","Prefer Option-returning tier functions so new turn kinds degrade to 'skipped', not a panic.","Include evidence-free turns in fixtures to assert they are filtered before tiering."],"tags":["rust","but-agentlog","internal-invariant","unreachable","projection"],"backgroundTag":"internal-invariant-panic","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}