{"record":{"id":"aee15d5858d4a51f","repo":"jdx/mise","slug":"lockfile-attributed-to-unknown-workspace-proj","errorCode":null,"errorMessage":"lockfile {:?} attributed to unknown workspace project {id:?}","messagePattern":"lockfile (.+?) attributed to unknown workspace project (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/task/workspace.rs","lineNumber":904,"sourceCode":"                        .or_default()\n                        .insert(AffectedProjectReason::ChangedPath { path: path.clone() });\n                }\n            }\n        } else {\n            for id in self.projects.keys() {\n                projects.entry(id.clone()).or_default().extend(\n                    global_paths\n                        .iter()\n                        .cloned()\n                        .map(|path| AffectedProjectReason::GlobalPath { path }),\n                );\n            }\n        }\n\n        for (path, ids) in lockfile_projects {\n            for id in ids {\n                if !self.projects.contains_key(id) {\n                    bail!(\n                        \"lockfile {:?} attributed to unknown workspace project {id:?}\",\n                        path\n                    );\n                }\n                projects\n                    .entry(id.clone())\n                    .or_default()\n                    .insert(AffectedProjectReason::Lockfile { path: path.clone() });\n            }\n        }\n\n        let mut dependents = BTreeMap::<ProjectId, BTreeSet<ProjectId>>::new();\n        for project in self.projects() {\n            for dependency in &project.dependencies {\n                dependents\n                    .entry(dependency.clone())\n                    .or_default()\n                    .insert(project.id.clone());","sourceCodeStart":886,"sourceCodeEnd":922,"githubUrl":"https://github.com/jdx/mise/blob/9dcfcaa0dc8747a2577d3270b69bb9d8313b2807/src/task/workspace.rs#L886-L922","documentation":"affected_projects_for_changes() maps changed paths and provider-attributed lockfiles to affected projects. The lockfile_projects argument is a caller-supplied map from lockfile path to ProjectId set, and every ID in it must exist in the graph. If a lockfile is attributed to a project the graph does not know, mise bail!s with the lockfile path and the offending ID.","triggerScenarios":"Passing a lockfile_projects map whose IDs came from an earlier graph build (workspace membership changed since: crate renamed, module dropped from go.work, project removed via override), or from a different workspace root/provider set than the graph being queried.","commonSituations":"Monorepo tooling that caches 'which lockfile affects which project' across runs; reattributing lockfiles after pulling a branch that restructured packages; CI replaying an old attribution map against a fresh checkout.","solutions":["Recompute attribution at query time with graph.affected_projects_for_lockfile(providers, path, before, after) instead of reusing a stale map","Filter the map's IDs through graph.get() and drop (or re-resolve) unknown ones before calling affected_projects_for_changes","Invalidate stored lockfile attributions whenever workspace member manifests (Cargo.toml, go.work, package.json, pyproject.toml) change"],"exampleFix":"// before\nlet affected = graph.affected_projects_for_changes(root, paths, &[], &stale_lockfile_map)?;\n\n// after\nlet lockfile_map: BTreeMap<_, _> = stale_lockfile_map\n    .into_iter()\n    .map(|(path, ids)| {\n        (path, ids.into_iter().filter(|id| graph.get(id).is_some()).collect())\n    })\n    .collect();\nlet affected = graph.affected_projects_for_changes(root, paths, &[], &lockfile_map)?;","handlingStrategy":"validation","validationCode":"let lockfile_projects: BTreeMap<PathBuf, BTreeSet<ProjectId>> = raw_map\n    .into_iter()\n    .map(|(path, ids)| (path, ids.into_iter().filter(|id| graph.get(id).is_some()).collect()))\n    .collect();\nlet affected = graph.affected_projects_for_changes(root, paths, global_inputs, &lockfile_projects)?;","typeGuard":"fn lockfile_map_is_current(graph: &WorkspaceProjectGraph, map: &BTreeMap<PathBuf, BTreeSet<ProjectId>>) -> bool {\n    map.values().flatten().all(|id| graph.get(id).is_some())\n}","tryCatchPattern":"match graph.affected_projects_for_changes(root, paths, gi, &map) {\n    Ok(res) => res,\n    Err(err) if err.to_string().contains(\"attributed to unknown workspace project\") => {\n        graph.affected_projects_for_changes(root, paths, gi, &BTreeMap::new())? // rebuild without stale attribution\n    }\n    Err(err) => return Err(err),\n}","preventionTips":["Recompute lockfile attribution with affected_projects_for_lockfile at query time instead of persisting ID maps","Key caches by a fingerprint of workspace member manifests, not just the lockfile hash","Treat any project rename/removal as a cache-invalidating event for lockfile attribution"],"tags":["rust","mise","workspace","lockfile","monorepo"],"backgroundTag":"unknown-project-reference","analyzedSha":"9dcfcaa0dc8747a2577d3270b69bb9d8313b2807","analyzedAt":"2026-08-17T14:28:50.624Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}