2026-08-15 19:46:07 +07:00
from __future__ import annotations
import hashlib
import json
from typing import Any
from control_plane . agents . models import ProgenySignal
from control_plane . events . bus import EventBus
from control_plane . projects . models import (
Decision ,
EvolutionCandidate ,
ExtensionCandidate ,
ExplorationOpportunity ,
Project ,
RoadmapHorizon ,
RoadmapItem ,
RoadmapStatus ,
RoadmapTargetAction ,
StewardFinding ,
)
from agents . lifecycle import EvolutionService , ExtensionService , ProjectContextMixin
from agents . progeny import ProgenyService
from model_router . router import ModelCapability , ModelRequestContract , ModelRouter
class RoadmapService ( ProjectContextMixin ) :
def __init__ ( self , router : ModelRouter | None = None , bus : EventBus | None = None ) - > None :
self . router = router
self . bus = bus or EventBus ( )
def upsert_item ( self , project : Project , * , title : str , description : str = " " , source : str = " USER " , source_ref : dict [ str , object ] | None = None , rationale : str = " " , evidence : dict [ str , object ] | None = None , horizon : str = RoadmapHorizon . EXPLORING , category : str = " " , target_action : str = RoadmapTargetAction . NONE , scores : dict [ str , object ] | None = None , status : str = RoadmapStatus . PROPOSED ) - > RoadmapItem :
grouping_key = self . _grouping_key ( project , title , category or target_action )
existing = self . _find_existing ( project , grouping_key , title )
values = self . score ( scores or { } )
if existing :
metadata = dict ( existing . metadata )
metadata [ " occurrences " ] = int ( metadata . get ( " occurrences " , 1 ) ) + 1
metadata . setdefault ( " reinforced_by " , [ ] ) . append ( { " source " : source , " source_ref " : source_ref or { } } )
existing . evidence = self . _merge_evidence ( existing . evidence , evidence or { } )
existing . source_ref = self . _merge_evidence ( existing . source_ref , source_ref or { } )
existing . confidence = min ( 1.0 , max ( existing . confidence , values [ " confidence " ] ) + 0.05 )
existing . composite_score = self . _composite ( existing )
existing . metadata = metadata
existing . save ( update_fields = [ " evidence " , " source_ref " , " confidence " , " composite_score " , " metadata " , " updated_at " ] )
self . bus . publish ( " ROADMAP_ITEM_UPDATED " , project = project , payload = { " roadmap_item_id " : str ( existing . id ) , " reason " : " deduplicated_reinforcement " } )
return existing
item = RoadmapItem . objects . create ( project = project , title = title , description = description , source = source , source_ref = source_ref or { } , rationale = rationale , evidence = evidence or { } , horizon = horizon , category = category , status = status , target_action = target_action , grouping_key = grouping_key , value_score = values [ " value " ] , effort_score = values [ " effort " ] , risk_score = values [ " risk " ] , confidence = values [ " confidence " ] , strategic_fit = values [ " strategic_fit " ] , technical_fit = values [ " technical_fit " ] , urgency = values [ " urgency " ] )
item . composite_score = self . _composite ( item )
item . priority = max ( 1 , min ( 100 , int ( item . composite_score * 100 ) ) )
item . save ( update_fields = [ " composite_score " , " priority " , " updated_at " ] )
self . bus . publish ( " ROADMAP_ITEM_CREATED " , project = project , payload = { " roadmap_item_id " : str ( item . id ) , " source " : source } )
return item
def gather_candidate_items ( self , project : Project ) - > list [ RoadmapItem ] :
items : list [ RoadmapItem ] = [ ]
for opportunity in project . exploration_opportunities . exclude ( status = " REJECTED " ) :
items . append ( self . upsert_item ( project , title = opportunity . title , description = opportunity . description , source = " EXPLORE " , source_ref = { " exploration_opportunity_id " : str ( opportunity . id ) } , rationale = opportunity . rationale , evidence = opportunity . evidence , horizon = RoadmapHorizon . EXPLORING , category = opportunity . opportunity_type , target_action = opportunity . recommended_action if opportunity . recommended_action in RoadmapTargetAction . values else RoadmapTargetAction . NONE , scores = { " value " : opportunity . value_score , " effort " : opportunity . effort_score , " risk " : opportunity . risk_score , " confidence " : opportunity . confidence , " strategic_fit " : opportunity . strategic_fit , " technical_fit " : opportunity . technical_fit } ) )
for finding in project . steward_findings . exclude ( status__in = [ " RESOLVED " , " DISMISSED " ] ) :
action = finding . recommended_action if finding . recommended_action in RoadmapTargetAction . values else RoadmapTargetAction . INVESTIGATE
items . append ( self . upsert_item ( project , title = finding . title , description = finding . summary , source = " STEWARD " , source_ref = { " steward_finding_id " : str ( finding . id ) } , rationale = " Steward surfaced this future project intent. " , evidence = finding . evidence , horizon = RoadmapHorizon . EXPLORING , category = finding . finding_type , target_action = action , scores = { " confidence " : finding . confidence , " risk " : 0.7 if finding . severity in [ " HIGH " , " CRITICAL " ] else 0.4 , " urgency " : 0.8 if finding . severity in [ " HIGH " , " CRITICAL " ] else 0.4 } ) )
return items
def review_with_project_brain ( self , project : Project ) - > dict [ str , object ] :
fallback = { " recommendations " : [ ] }
if self . router is None :
return fallback
payload = { " context " : self . project_context ( project ) , " roadmap_items " : list ( project . roadmap_items . values ( " id " , " title " , " description " , " horizon " , " status " , " target_action " , " value_score " , " effort_score " , " risk_score " , " confidence " , " strategic_fit " , " technical_fit " , " urgency " , " composite_score " ) ) }
try :
2026-08-16 15:35:55 +07:00
response = self . router . complete ( ModelRequestContract ( purpose = ModelCapability . PLANNING , project = project , prompt = " Review this existing project roadmap. Return JSON with recommendations: item_id, recommendation, rationale, optional horizon/status. Do not execute work. \n " + json . dumps ( payload , default = str ) ) )
2026-08-15 19:46:07 +07:00
parsed = json . loads ( response . content )
return parsed if isinstance ( parsed , dict ) else fallback
except Exception as exc :
ProgenySignal . objects . create ( project = project , source = " roadmap " , severity = " MEDIUM " , failure_category = " ROADMAP_PRIORITIZATION " , summary = " Roadmap Project Brain review failed; deterministic scoring retained. " , evidence = { " error " : str ( exc ) } , grouping_key = f " roadmap: { project . id } :prioritization " )
return fallback
def apply_recommendations ( self , project : Project , recommendations : dict [ str , object ] ) - > list [ RoadmapItem ] :
updated : list [ RoadmapItem ] = [ ]
for rec in recommendations . get ( " recommendations " , [ ] ) :
if not isinstance ( rec , dict ) :
continue
item_id = rec . get ( " item_id " )
try :
item = project . roadmap_items . get ( id = item_id )
except Exception :
continue
item . metadata = { * * item . metadata , " project_brain_recommendations " : [ * item . metadata . get ( " project_brain_recommendations " , [ ] ) , rec ] }
if rec . get ( " horizon " ) in RoadmapHorizon . values :
item . horizon = str ( rec [ " horizon " ] )
if rec . get ( " status " ) in RoadmapStatus . values :
item . status = str ( rec [ " status " ] )
item . save ( update_fields = [ " horizon " , " status " , " metadata " , " updated_at " ] )
updated . append ( item )
self . bus . publish ( " ROADMAP_ITEM_UPDATED " , project = project , payload = { " roadmap_item_id " : str ( item . id ) , " recommendation " : rec } )
return updated
def review_project_roadmap ( self , project : Project ) - > dict [ str , object ] :
self . gather_candidate_items ( project )
recommendations = self . review_with_project_brain ( project )
self . apply_recommendations ( project , recommendations )
self . bus . publish ( " ROADMAP_REVIEW_COMPLETED " , project = project , payload = { " item_count " : project . roadmap_items . count ( ) , " recommendations " : recommendations } )
return self . project_roadmap_view ( project )
def convert_to_extension ( self , item : RoadmapItem ) - > ExtensionCandidate :
candidate = ExtensionService ( bus = self . bus ) . create_candidate ( item . project , title = item . title , description = item . description , rationale = item . rationale , source = " RoadmapItem " , expected_value = str ( item . evidence . get ( " expected_value " , " " ) ) , affected_areas = [ item . category ] if item . category else [ ] , risk = str ( item . risk_score ) , confidence = item . confidence , evidence = { " roadmap_item_id " : str ( item . id ) , * * item . evidence } , source_roadmap_item = item )
item . converted_extension = candidate
item . status = RoadmapStatus . PLANNING
item . save ( update_fields = [ " converted_extension " , " status " , " updated_at " ] )
self . bus . publish ( " ROADMAP_ITEM_CONVERTED " , project = item . project , payload = { " roadmap_item_id " : str ( item . id ) , " extension_candidate_id " : str ( candidate . id ) } )
return candidate
def convert_to_evolution ( self , item : RoadmapItem , * , baseline_measurement : dict [ str , object ] , desired_direction : str = " DECREASE " ) - > EvolutionCandidate :
candidate = EvolutionService ( bus = self . bus ) . create_candidate ( item . project , target = item . category or item . title , objective = item . description or item . title , baseline_measurement = baseline_measurement , desired_direction = desired_direction , rationale = item . rationale , source = " RoadmapItem " , evidence = { " roadmap_item_id " : str ( item . id ) , * * item . evidence } , risk = str ( item . risk_score ) , confidence = item . confidence , source_roadmap_item = item )
item . converted_evolution = candidate
item . status = RoadmapStatus . PLANNING
item . save ( update_fields = [ " converted_evolution " , " status " , " updated_at " ] )
self . bus . publish ( " ROADMAP_ITEM_CONVERTED " , project = item . project , payload = { " roadmap_item_id " : str ( item . id ) , " evolution_candidate_id " : str ( candidate . id ) } )
return candidate
def convert_to_investigation ( self , item : RoadmapItem ) :
signal = ProgenySignal . objects . create ( project = item . project , source = " roadmap " , severity = " MEDIUM " , failure_category = item . category or " ROADMAP_INVESTIGATION " , summary = item . description or item . title , evidence = { " roadmap_item_id " : str ( item . id ) , * * item . evidence } , grouping_key = f " roadmap: { item . grouping_key } " [ : 120 ] )
investigation = ProgenyService ( self . bus ) . create_smart_investigation ( signal . grouping_key )
item . converted_investigation = investigation
item . status = RoadmapStatus . PLANNING
item . save ( update_fields = [ " converted_investigation " , " status " , " updated_at " ] )
self . bus . publish ( " ROADMAP_ITEM_CONVERTED " , project = item . project , payload = { " roadmap_item_id " : str ( item . id ) , " investigation_id " : str ( investigation . id ) } )
return investigation
def project_roadmap_view ( self , project : Project ) - > dict [ str , object ] :
return { horizon : [ self . _item ( item ) for item in project . roadmap_items . filter ( horizon = horizon ) . order_by ( " -composite_score " , " -priority " , " created_at " ) ] for horizon in RoadmapHorizon . values }
def score ( self , raw : dict [ str , object ] ) - > dict [ str , float ] :
return { key : self . _score_value ( raw . get ( key , 0.5 ) ) for key in [ " value " , " effort " , " risk " , " confidence " , " strategic_fit " , " technical_fit " , " urgency " ] }
def _score_value ( self , value : object ) - > float :
try :
score = float ( value )
except ( TypeError , ValueError ) :
return 0.5
return max ( 0.0 , min ( 1.0 , score ) )
def _composite ( self , item : RoadmapItem ) - > float :
return ( item . value_score * 0.25 ) + ( ( 1 - item . effort_score ) * 0.12 ) + ( ( 1 - item . risk_score ) * 0.12 ) + ( item . confidence * 0.14 ) + ( item . strategic_fit * 0.14 ) + ( item . technical_fit * 0.11 ) + ( item . urgency * 0.12 )
def _grouping_key ( self , project : Project , title : str , category : str ) - > str :
fingerprint = hashlib . sha256 ( f " { title . lower ( ) } : { category . lower ( ) } " . encode ( " utf-8 " ) ) . hexdigest ( ) [ : 16 ]
return f " { project . id } :roadmap: { fingerprint } " [ : 240 ]
def _find_existing ( self , project : Project , grouping_key : str , title : str ) - > RoadmapItem | None :
existing = project . roadmap_items . filter ( grouping_key = grouping_key ) . first ( ) or project . roadmap_items . filter ( title__iexact = title ) . first ( )
if existing :
return existing
if project . exploration_opportunities . filter ( title__iexact = title ) . exists ( ) or ExtensionCandidate . objects . filter ( project = project , title__iexact = title ) . exists ( ) or EvolutionCandidate . objects . filter ( project = project , objective__icontains = title [ : 80 ] ) . exists ( ) or StewardFinding . objects . filter ( project = project , title__iexact = title ) . exists ( ) :
return project . roadmap_items . filter ( title__iexact = title ) . first ( )
if Decision . objects . filter ( project = project , decision__icontains = title [ : 80 ] , decision_type__in = [ " REJECTED " , " DEFERRED " ] ) . exists ( ) :
return project . roadmap_items . filter ( title__iexact = title ) . first ( )
return None
def _merge_evidence ( self , current : dict [ str , object ] , incoming : dict [ str , object ] ) - > dict [ str , object ] :
merged = dict ( current or { } )
for key , value in incoming . items ( ) :
if key in merged and merged [ key ] != value :
merged [ key ] = [ merged [ key ] , value ]
else :
merged [ key ] = value
return merged
def _item ( self , item : RoadmapItem ) - > dict [ str , object ] :
return { " id " : str ( item . id ) , " title " : item . title , " source " : item . source , " rationale " : item . rationale , " evidence " : item . evidence , " scores " : { " value " : item . value_score , " effort " : item . effort_score , " risk " : item . risk_score , " confidence " : item . confidence , " strategic_fit " : item . strategic_fit , " technical_fit " : item . technical_fit , " urgency " : item . urgency , " composite " : item . composite_score } , " status " : item . status , " target_action " : item . target_action , " dependencies " : [ str ( dep . id ) for dep in item . dependencies . all ( ) ] , " related_items " : [ str ( rel . id ) for rel in item . related_items . all ( ) ] , " conversion_lineage " : { " extension_candidate_id " : str ( item . converted_extension_id ) if item . converted_extension_id else None , " evolution_candidate_id " : str ( item . converted_evolution_id ) if item . converted_evolution_id else None , " investigation_id " : str ( item . converted_investigation_id ) if item . converted_investigation_id else None } , " metadata " : item . metadata }