{"record":{"id":"3d7e8ced8928c8c0","repo":"tinyhumansai/openhuman","slug":"filter-provider-does-not-match-source-provide","errorCode":null,"errorMessage":"filter provider '{}' does not match source provider '{}'","messagePattern":"filter provider '(.+?)' does not match source provider '(.+?)'","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"warning","filePath":"src/openhuman/integrations/task_sources/store.rs","lineNumber":73,"sourceCode":"    );\n    let digest = Sha256::digest(canonical.as_bytes());\n    format!(\"{digest:x}\")\n}\n\n/// Insert a new task source.\n#[allow(clippy::too_many_arguments)]\npub fn add_source(\n    config: &Config,\n    provider: ProviderSlug,\n    connection_id: Option<String>,\n    name: Option<String>,\n    filter: FilterSpec,\n    interval_secs: u64,\n    target: SourceTarget,\n    max_tasks_per_fetch: u32,\n) -> Result<TaskSource> {\n    if filter.provider() != provider {\n        anyhow::bail!(\n            \"filter provider '{}' does not match source provider '{}'\",\n            filter.provider().as_str(),\n            provider.as_str()\n        );\n    }\n    // Normalize blank optional fields to NULL so a whitespace-only\n    // connection_id can't masquerade as a real selector (mirrors\n    // `update_source`).\n    let connection_id = connection_id.filter(|s| !s.trim().is_empty());\n    let name = name.filter(|s| !s.trim().is_empty());\n    let id = Uuid::new_v4().to_string();\n    let now = Utc::now();\n    let filter_json = serde_json::to_string(&filter).context(\"serialize task source filter\")?;\n    let target_json = serde_json::to_string(&target).context(\"serialize task source target\")?;\n    let interval_i64 = i64::try_from(interval_secs)\n        .context(\"task source interval_secs exceeds SQLite INTEGER range\")?;\n\n    with_connection(config, |conn| {","sourceCodeStart":55,"sourceCodeEnd":91,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/src/openhuman/integrations/task_sources/store.rs#L55-L91","documentation":"Contract check in task_sources::store::add_source: FilterSpec is a provider-tagged enum (github|notion|linear|clickup) and its tag must equal the provider argument of the source being created. The store refuses to persist a source whose filter targets a different provider, because every downstream fetcher resolves the provider registry from TaskSource.provider and would misinterpret the filter.","triggerScenarios":"add_source(config, ProviderSlug::Github, ...) called with FilterSpec::Notion{..} (or any cross pair). Typically happens when the provider is chosen independently in the UI while the filter JSON was deserialized from a stale form or a different tab's picker.","commonSituations":"Frontend sends {provider:'linear', filter:{provider:'github',...}} after the user switched provider without resetting the filter; copy-pasting a source template and editing only the provider; hand-built RPC payloads.","solutions":["Derive the provider from the filter itself — add_source(config, filter.provider(), ...) — so the two can never diverge","If the provider is the source of truth, rebuild the filter as the matching variant (FilterSpec::Github for ProviderSlug::Github)","In the frontend, reset the filter form whenever the provider selection changes","Validate the pair before persisting and return a field-level error to the user"],"exampleFix":"// before — hardcoded provider can drift from the filter variant\nlet provider = ProviderSlug::Linear;\nadd_source(&config, provider, conn_id, name, filter, 300, target, 25)?;\n\n// after — derive the provider from the filter tag\nadd_source(&config, filter.provider(), conn_id, name, filter.clone(), 300, target, 25)?;","handlingStrategy":"type-guard","validationCode":"if filter.provider() != provider {\n    anyhow::bail!(\n        \"filter targets {} but source provider is {} — rebuild the filter for the selected provider\",\n        filter.provider().as_str(),\n        provider.as_str()\n    );\n}","typeGuard":"fn filter_matches_source(filter: &FilterSpec, provider: ProviderSlug) -> bool {\n    filter.provider() == provider\n}","tryCatchPattern":"match add_source(&config, provider, conn, name, filter, interval, target, max).await {\n    Ok(src) => Ok(src),\n    Err(e) if format!(\"{e:#}\").contains(\"does not match source provider\") => {\n        // UI sent a stale filter for a different provider — reset the filter form, not a retry\n        Err(anyhow::anyhow!(\"filter/provider mismatch: reset the filter for the selected provider\"))\n    }\n    Err(e) => Err(e),\n}","preventionTips":["Derive the provider argument from the filter tag (filter.provider()) instead of passing them independently","Reset the filter form in the UI whenever the provider selection changes","Validate the pair at the RPC boundary and return a field-level error naming both providers"],"tags":["task-sources","validation","provider-mismatch","filter","argument-contract"],"backgroundTag":"provider-mismatch-validation","analyzedSha":"749120085864ce16e0f273c7b86fac7740b39c5b","analyzedAt":"2026-08-17T21:21:45.363Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}