{"record":{"id":"1fe4ea259d15cf71","repo":"jdx/mise","slug":"workspace-provider-attributed-lockfile-t-1fe4ea","errorCode":null,"errorMessage":"workspace provider {:?} attributed lockfile {:?} to unknown project {id:?}","messagePattern":"workspace provider (.+?) attributed lockfile (.+?) to unknown project (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/task/workspace.rs","lineNumber":971,"sourceCode":"        let mut affected = BTreeSet::new();\n        for provider in providers {\n            let Some(projects) =\n                provider.affected_projects_for_lockfile(lockfile_path, before, after, self)?\n            else {\n                continue;\n            };\n            recognized = true;\n            let expected_prefix = format!(\"{}:\", provider.id());\n            for id in projects {\n                if !id.as_str().starts_with(&expected_prefix) {\n                    bail!(\n                        \"workspace provider {:?} attributed lockfile {:?} to foreign project {id:?}\",\n                        provider.id(),\n                        lockfile_path\n                    );\n                }\n                if !self.projects.contains_key(&id) {\n                    bail!(\n                        \"workspace provider {:?} attributed lockfile {:?} to unknown project {id:?}\",\n                        provider.id(),\n                        lockfile_path\n                    );\n                }\n                affected.insert(id);\n            }\n        }\n        Ok(recognized.then_some(affected))\n    }\n\n    /// Summarizes provider failures retained during lenient task discovery.\n    pub(crate) fn provider_discovery_error(&self) -> Option<String> {\n        (!self.provider_errors.is_empty()).then(|| {\n            self.provider_errors\n                .iter()\n                .map(|(provider, error)| format!(\"{provider}: {error}\"))\n                .collect::<Vec<_>>()","sourceCodeStart":953,"sourceCodeEnd":989,"githubUrl":"https://github.com/jdx/mise/blob/9dcfcaa0dc8747a2577d3270b69bb9d8313b2807/src/task/workspace.rs#L953-L989","documentation":"WorkspaceProjectGraph::affected_projects_for_lockfile() validates that each ID a provider returns is both correctly namespaced AND present in the graph's discovered projects. This variant fires when the namespace is right but the project itself was never discovered — the provider attributed a lockfile to one of its own projects that the graph does not contain.","triggerScenarios":"A provider's lockfile attribution reads the lockfile contents (e.g. package names recorded in a lockfile) and returns an ID for a package that discovery skipped: the member is excluded from workspace members, its manifest was filtered out, the project was removed via [monorepo.projects] override, or discovery ran in lenient mode and that provider partially failed.","commonSituations":"Lockfile still lists a package after it was removed from pnpm-workspace.yaml / Cargo members; monorepo shrink during a refactor; provider attribution logic reading stale lockfile entries ('before'/'after' snapshots spanning a membership change).","solutions":["Make the provider only attribute projects it would itself discover (cross-check its own discover() result before returning)","Rebuild the graph from the same workspace root and provider set used for attribution so discovery and attribution agree","Strip lockfile-attributed IDs that do not resolve via graph.get() before merging them into lockfile_projects"],"exampleFix":"// before (inside a custom provider)\nlet mut ids = BTreeSet::new();\nfor name in lockfile_packages { ids.insert(name.parse()?); }\nOk(Some(ids))\n\n// after\nlet discovered: BTreeSet<ProjectId> = self.discover(root)?.into_iter().map(|p| p.id).collect();\nlet ids = lockfile_packages.iter().filter_map(|n| n.parse().ok()).filter(|id| discovered.contains(id)).collect();\nOk(Some(ids))","handlingStrategy":"validation","validationCode":"// inside a provider: only attribute projects you would discover\nlet discovered: BTreeSet<ProjectId> = self.discover(workspace_root)?.into_iter().map(|p| p.id).collect();\nlet ids: BTreeSet<ProjectId> = attributed.into_iter().filter(|id| discovered.contains(id)).collect();\nOk((!ids.is_empty()).then_some(ids))","typeGuard":"fn attribution_is_discoverable(provider: &dyn WorkspaceProvider, root: &Path, ids: &BTreeSet<ProjectId>) -> bool {\n    let discovered: BTreeSet<ProjectId> = provider.discover(root).map(|ps| ps.into_iter().map(|p| p.id).collect()).unwrap_or_default();\n    ids.iter().all(|id| discovered.contains(id))\n}","tryCatchPattern":"match graph.affected_projects_for_lockfile(providers, path, before, after) {\n    Ok(res) => res,\n    Err(err) if err.to_string().contains(\"attributed lockfile\") => {\n        warn!(\"provider attribution out of sync: {err}\");\n        None\n    }\n    Err(err) => return Err(err),\n}","preventionTips":["Derive lockfile attribution and discovery from the same manifest-parsing code path","Regenerate the graph after workspace membership edits before processing lockfile changes","Filter lockfile-parsed package names through the current discovery result, not raw lockfile contents"],"tags":["rust","mise","workspace","lockfile","provider"],"backgroundTag":"unknown-project-reference","analyzedSha":"9dcfcaa0dc8747a2577d3270b69bb9d8313b2807","analyzedAt":"2026-08-17T14:28:50.624Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}