66from pathlib import Path
77from typing import Any
88
9- SCHEMA_VERSION = "0.6 .0"
9+ SCHEMA_VERSION = "0.7 .0"
1010
1111# The published "latest" portfolio-truth artifact. The producer
1212# (portfolio_truth_publish) writes it; every reader resolves it through
@@ -32,7 +32,13 @@ def truth_latest_path(output_dir: Path) -> Path:
3232 "evidence-history" ,
3333 "manual-only" ,
3434}
35- VALID_LIFECYCLE_STATES = {"active" , "maintenance" , "dormant" , "experimental" , "archived" }
35+ VALID_LIFECYCLE_STATES = {
36+ "active" ,
37+ "maintenance" ,
38+ "dormant" ,
39+ "experimental" ,
40+ "archived" ,
41+ }
3642VALID_CATEGORY_TAGS = {
3743 "commercial" ,
3844 "it-work" ,
@@ -129,7 +135,9 @@ class DerivedFields:
129135
130136 def to_dict (self ) -> dict [str , Any ]:
131137 data = dataclasses .asdict (self )
132- data ["last_meaningful_activity_at" ] = _serialize_datetime (self .last_meaningful_activity_at )
138+ data ["last_meaningful_activity_at" ] = _serialize_datetime (
139+ self .last_meaningful_activity_at
140+ )
133141 return data
134142
135143
@@ -183,7 +191,9 @@ def open_high_critical(self) -> int:
183191 return self .dependabot_high + self .dependabot_critical
184192
185193 def to_dict (self ) -> dict [str , Any ]:
186- return dataclasses .asdict (self )
194+ data = dataclasses .asdict (self )
195+ data ["open_high_critical" ] = self .open_high_critical
196+ return data
187197
188198
189199@dataclass (frozen = True )
@@ -210,6 +220,71 @@ def to_dict(self) -> dict[str, Any]:
210220 }
211221
212222
223+ @dataclass (frozen = True )
224+ class PortfolioTruthRollups :
225+ """Portfolio-level aggregates derived from the project list, emitted so
226+ downstream consumers (command-center, dashboards) read them instead of
227+ re-deriving the auditor's risk/security logic, which is the #1 drift risk."""
228+
229+ risk_tier_counts : dict [str , int ]
230+ security : dict [str , int ]
231+ decision : dict [str , int ]
232+
233+ @classmethod
234+ def from_projects (
235+ cls , projects : list [PortfolioTruthProject ]
236+ ) -> PortfolioTruthRollups :
237+ risk_tier_counts = {
238+ "elevated" : 0 ,
239+ "moderate" : 0 ,
240+ "baseline" : 0 ,
241+ "deferred" : 0 ,
242+ }
243+ scanned_count = 0
244+ repos_with_open_high_critical = 0
245+ total_open_high = 0
246+ total_open_critical = 0
247+ decision_needed_count = 0
248+ default_attention_count = 0
249+ for project in projects :
250+ tier = project .risk .risk_tier
251+ if tier in risk_tier_counts :
252+ risk_tier_counts [tier ] += 1
253+ security = project .security
254+ if security .alerts_available :
255+ scanned_count += 1
256+ if security .open_high_critical > 0 :
257+ repos_with_open_high_critical += 1
258+ total_open_high += security .dependabot_high
259+ total_open_critical += security .dependabot_critical
260+ attention = project .derived .attention_state
261+ if attention == "decision-needed" :
262+ decision_needed_count += 1
263+ default_attention_count += 1
264+ elif attention in ("active-product" , "active-infra" ):
265+ default_attention_count += 1
266+ return cls (
267+ risk_tier_counts = risk_tier_counts ,
268+ security = {
269+ "scanned_count" : scanned_count ,
270+ "repos_with_open_high_critical" : repos_with_open_high_critical ,
271+ "total_open_high" : total_open_high ,
272+ "total_open_critical" : total_open_critical ,
273+ },
274+ decision = {
275+ "decision_needed_count" : decision_needed_count ,
276+ "default_attention_count" : default_attention_count ,
277+ },
278+ )
279+
280+ def to_dict (self ) -> dict [str , Any ]:
281+ return {
282+ "risk_tier_counts" : dict (self .risk_tier_counts ),
283+ "security" : dict (self .security ),
284+ "decision" : dict (self .decision ),
285+ }
286+
287+
213288@dataclass (frozen = True )
214289class PortfolioTruthSnapshot :
215290 schema_version : str
@@ -219,6 +294,12 @@ class PortfolioTruthSnapshot:
219294 precedence_matrix : dict [str , list [str ]]
220295 warnings : list [str ]
221296 projects : list [PortfolioTruthProject ]
297+ rollups : PortfolioTruthRollups = field (init = False )
298+
299+ def __post_init__ (self ) -> None :
300+ object .__setattr__ (
301+ self , "rollups" , PortfolioTruthRollups .from_projects (self .projects )
302+ )
222303
223304 def to_dict (self ) -> dict [str , Any ]:
224305 return {
@@ -229,4 +310,5 @@ def to_dict(self) -> dict[str, Any]:
229310 "precedence_matrix" : self .precedence_matrix ,
230311 "warnings" : list (self .warnings ),
231312 "projects" : [project .to_dict () for project in self .projects ],
313+ "rollups" : self .rollups .to_dict (),
232314 }
0 commit comments