Count what still refers to each published workspace asset.
A local copy may be removed once nothing local needs it and a verified remote copy exists. Runs that are themselves cold still reference the asset - that is what keeps the remote copy alive - but they do not keep the local one.
41def workspace_asset_references(workspace_root: str) -> dict:
42 """!
43 @brief Count what still refers to each published workspace asset.
44
45 @details A local copy may be removed once nothing local needs it and a verified
46 remote copy exists. Runs that are themselves cold still *reference* the
47 asset - that is what keeps the remote copy alive - but they do not keep
48 the local one.
49 @param[in] workspace_root Initialized workspace root.
50 @return Mapping of asset id to its reference counts and local object path.
51 """
52 references = {}
53 objects_root = os.path.join(workspace_root, "assets", "objects")
54 if os.path.isdir(objects_root):
55 for kind in sorted(os.listdir(objects_root)):
56 kind_root = os.path.join(objects_root, kind)
57 if not os.path.isdir(kind_root):
58 continue
59 for asset_id in sorted(os.listdir(kind_root)):
60 object_root = os.path.join(kind_root, asset_id)
61 if os.path.isdir(object_root):
62 references[asset_id] = {
63 "asset_id": asset_id, "kind": kind, "object": object_root,
64 "active_local_runs": 0, "cold_runs": 0,
65 }
66 for artifacts in ("runs", "studies"):
67 root = os.path.join(workspace_root, artifacts)
68 if not os.path.isdir(root):
69 continue
70 for lock_path in Path(root).glob("**/inputs/assets.lock.yml"):
71 try:
72 with open(lock_path, "r", encoding="utf-8") as stream:
73 lock = yaml.safe_load(stream) or {}
74 except (OSError, ValueError):
75 continue
76 run_root = lock_path.parent.parent
77 cold = is_artifact_cold(str(run_root))
78 for reference in (lock.get("assets") or {}).values():
79 entry = references.get(reference.get("asset_id"))
80 if entry is None:
81 continue
82 entry["cold_runs" if cold else "active_local_runs"] += 1
83 return references
84
85