|
|
""" |
|
|
Kit Relationship Visualization |
|
|
Shows the actual dependency relationships between kits in production |
|
|
based on kit_hierarchy.json data |
|
|
""" |
|
|
|
|
|
import streamlit as st |
|
|
import pandas as pd |
|
|
import plotly.express as px |
|
|
import plotly.graph_objects as go |
|
|
from plotly.subplots import make_subplots |
|
|
import json |
|
|
import sys |
|
|
|
|
|
from src.config.constants import ShiftType, LineType, KitLevel |
|
|
|
|
|
|
|
|
try: |
|
|
import networkx as nx |
|
|
NETWORKX_AVAILABLE = True |
|
|
except ImportError: |
|
|
NETWORKX_AVAILABLE = False |
|
|
nx = None |
|
|
|
|
|
def load_kit_hierarchy(): |
|
|
"""Load kit hierarchy data from JSON file""" |
|
|
try: |
|
|
with open('data/hierarchy_exports/kit_hierarchy.json', 'r') as f: |
|
|
return json.load(f) |
|
|
except FileNotFoundError: |
|
|
st.error("Kit hierarchy file not found. Please ensure kit_hierarchy.json exists in data/hierarchy_exports/") |
|
|
return {} |
|
|
except json.JSONDecodeError: |
|
|
st.error("Invalid kit hierarchy JSON format") |
|
|
return {} |
|
|
|
|
|
def display_kit_relationships_dashboard(results): |
|
|
"""Main dashboard showing kit relationships in production""" |
|
|
st.header("π Kit Relationship Dashboard") |
|
|
st.markdown("Visualizing dependencies between kits being produced") |
|
|
st.markdown("---") |
|
|
|
|
|
|
|
|
hierarchy_data = load_kit_hierarchy() |
|
|
|
|
|
if not hierarchy_data: |
|
|
st.warning("No kit hierarchy data available") |
|
|
return |
|
|
|
|
|
|
|
|
produced_kits = set() |
|
|
if 'weekly_production' in results: |
|
|
produced_kits = set(results['weekly_production'].keys()) |
|
|
elif 'run_schedule' in results: |
|
|
produced_kits = set(row['product'] for row in results['run_schedule']) |
|
|
|
|
|
if not produced_kits: |
|
|
st.warning("No production data available") |
|
|
return |
|
|
|
|
|
|
|
|
tab1, tab2, tab3, tab4 = st.tabs([ |
|
|
"π Dependency Network", |
|
|
"π Relationship Matrix", |
|
|
"π― Production Flow", |
|
|
"β οΈ Dependency Analysis" |
|
|
]) |
|
|
|
|
|
with tab1: |
|
|
display_dependency_network(hierarchy_data, produced_kits, results) |
|
|
|
|
|
with tab2: |
|
|
display_relationship_matrix(hierarchy_data, produced_kits, results) |
|
|
|
|
|
with tab3: |
|
|
display_production_flow_relationships(hierarchy_data, produced_kits, results) |
|
|
|
|
|
with tab4: |
|
|
display_dependency_analysis(hierarchy_data, produced_kits, results) |
|
|
|
|
|
def display_dependency_network(hierarchy_data, produced_kits, results): |
|
|
"""Show interactive network graph of kit dependencies""" |
|
|
st.subheader("π Kit Dependency Network") |
|
|
st.markdown("Interactive graph showing which kits depend on other kits") |
|
|
|
|
|
|
|
|
relationships = build_relationship_data(hierarchy_data, produced_kits) |
|
|
|
|
|
if not relationships: |
|
|
st.info("No dependency relationships found between produced kits") |
|
|
return |
|
|
|
|
|
|
|
|
production_timing = get_production_timing(results) |
|
|
|
|
|
|
|
|
col1, col2 = st.columns([3, 1]) |
|
|
|
|
|
with col1: |
|
|
if NETWORKX_AVAILABLE: |
|
|
fig = create_interactive_network_graph(relationships, production_timing) |
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
else: |
|
|
fig = create_simple_dependency_chart(relationships, production_timing) |
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
st.info("π‘ Install networkx for advanced network layouts: `pip install networkx`") |
|
|
|
|
|
with col2: |
|
|
|
|
|
st.subheader("π Network Stats") |
|
|
|
|
|
all_kits = set() |
|
|
for rel in relationships: |
|
|
all_kits.add(rel['source']) |
|
|
all_kits.add(rel['target']) |
|
|
|
|
|
st.metric("Total Kits", len(all_kits)) |
|
|
st.metric("Dependencies", len(relationships)) |
|
|
|
|
|
|
|
|
max_depth = calculate_dependency_depth(relationships) |
|
|
st.metric("Max Dependency Depth", max_depth) |
|
|
|
|
|
|
|
|
dependent_kits = get_most_dependent_kits(relationships) |
|
|
st.subheader("π Most Dependencies") |
|
|
for kit, count in dependent_kits[:5]: |
|
|
st.write(f"**{kit}**: {count} dependencies") |
|
|
|
|
|
def display_relationship_matrix(hierarchy_data, produced_kits, results): |
|
|
"""Show dependency matrix heatmap""" |
|
|
st.subheader("π Kit Dependency Matrix") |
|
|
st.markdown("Heatmap showing which kits (rows) depend on which other kits (columns)") |
|
|
|
|
|
|
|
|
matrix_data = build_dependency_matrix(hierarchy_data, produced_kits) |
|
|
|
|
|
if matrix_data.empty: |
|
|
st.info("No dependency relationships to visualize in matrix form") |
|
|
return |
|
|
|
|
|
|
|
|
fig = px.imshow(matrix_data.values, |
|
|
x=matrix_data.columns, |
|
|
y=matrix_data.index, |
|
|
color_continuous_scale='Blues', |
|
|
title='Kit Dependency Matrix (1 = depends on, 0 = no dependency)', |
|
|
labels=dict(x="Dependency (what is needed)", |
|
|
y="Kit (what depends on others)", |
|
|
color="Dependency")) |
|
|
|
|
|
fig.update_layout(height=600) |
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
|
|
|
with st.expander("π View Dependency Matrix as Table"): |
|
|
st.dataframe(matrix_data, use_container_width=True) |
|
|
|
|
|
def display_production_flow_relationships(hierarchy_data, produced_kits, results): |
|
|
"""Show how relationships affect production timing""" |
|
|
st.subheader("π― Production Flow with Relationships") |
|
|
st.markdown("Timeline showing when dependent kits are produced") |
|
|
|
|
|
|
|
|
production_timing = get_production_timing(results) |
|
|
relationships = build_relationship_data(hierarchy_data, produced_kits) |
|
|
|
|
|
if not production_timing or not relationships: |
|
|
st.info("Insufficient data for production flow analysis") |
|
|
return |
|
|
|
|
|
|
|
|
fig = create_production_timeline_with_dependencies(production_timing, relationships) |
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
|
|
|
st.subheader("β° Dependency Timing Analysis") |
|
|
timing_analysis = analyze_dependency_timing(production_timing, relationships) |
|
|
|
|
|
if timing_analysis: |
|
|
df = pd.DataFrame(timing_analysis) |
|
|
st.dataframe(df, use_container_width=True) |
|
|
|
|
|
def display_dependency_analysis(hierarchy_data, produced_kits, results): |
|
|
"""Analyze dependency fulfillment and violations""" |
|
|
st.subheader("β οΈ Dependency Analysis & Violations") |
|
|
|
|
|
production_timing = get_production_timing(results) |
|
|
relationships = build_relationship_data(hierarchy_data, produced_kits) |
|
|
|
|
|
|
|
|
violations = find_dependency_violations(production_timing, relationships) |
|
|
|
|
|
|
|
|
col1, col2, col3, col4 = st.columns(4) |
|
|
|
|
|
with col1: |
|
|
total_deps = len(relationships) |
|
|
st.metric("Total Dependencies", total_deps) |
|
|
|
|
|
with col2: |
|
|
violated_deps = len(violations) |
|
|
st.metric("Violations", violated_deps, |
|
|
delta=f"-{violated_deps}" if violated_deps > 0 else None) |
|
|
|
|
|
with col3: |
|
|
if total_deps > 0: |
|
|
success_rate = ((total_deps - violated_deps) / total_deps) * 100 |
|
|
st.metric("Success Rate", f"{success_rate:.1f}%") |
|
|
else: |
|
|
st.metric("Success Rate", "N/A") |
|
|
|
|
|
with col4: |
|
|
if violations: |
|
|
avg_violation = sum(v['days_early'] for v in violations) / len(violations) |
|
|
st.metric("Avg Days Early", f"{avg_violation:.1f}") |
|
|
else: |
|
|
st.metric("Avg Days Early", "0") |
|
|
|
|
|
|
|
|
if violations: |
|
|
st.subheader("π¨ Dependency Violations") |
|
|
st.markdown("Cases where kits were produced before their dependencies") |
|
|
|
|
|
violation_df = pd.DataFrame(violations) |
|
|
|
|
|
|
|
|
fig = px.scatter(violation_df, |
|
|
x='dependency_day', y='kit_day', |
|
|
size='days_early', color='severity', |
|
|
hover_data=['kit', 'dependency'], |
|
|
title='Dependency Violations (Below diagonal = violation)', |
|
|
labels={'dependency_day': 'When Dependency Was Made', |
|
|
'kit_day': 'When Kit Was Made'}) |
|
|
|
|
|
|
|
|
max_day = max(violation_df['dependency_day'].max(), violation_df['kit_day'].max()) |
|
|
fig.add_shape(type="line", x0=0, y0=0, x1=max_day, y1=max_day, |
|
|
line=dict(dash="dash", color="green"), |
|
|
name="Ideal Timeline") |
|
|
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
|
|
|
st.dataframe(violation_df[['kit', 'dependency', 'kit_day', 'dependency_day', |
|
|
'days_early', 'severity']], use_container_width=True) |
|
|
else: |
|
|
st.success("π No dependency violations found! All kits produced in correct order.") |
|
|
|
|
|
|
|
|
st.subheader("π‘ Recommendations") |
|
|
recommendations = generate_dependency_recommendations(violations, relationships, production_timing) |
|
|
for rec in recommendations: |
|
|
st.info(f"π‘ {rec}") |
|
|
|
|
|
|
|
|
|
|
|
def build_relationship_data(hierarchy_data, produced_kits): |
|
|
"""Build relationship data for visualization""" |
|
|
relationships = [] |
|
|
|
|
|
for kit_id, kit_info in hierarchy_data.items(): |
|
|
if kit_id not in produced_kits: |
|
|
continue |
|
|
|
|
|
|
|
|
dependencies = kit_info.get('dependencies', []) |
|
|
for dep in dependencies: |
|
|
if dep in produced_kits: |
|
|
relationships.append({ |
|
|
'source': dep, |
|
|
'target': kit_id, |
|
|
'type': 'direct', |
|
|
'source_type': hierarchy_data.get(dep, {}).get('type', 'unknown'), |
|
|
'target_type': kit_info.get('type', 'unknown') |
|
|
}) |
|
|
|
|
|
return relationships |
|
|
|
|
|
def build_dependency_matrix(hierarchy_data, produced_kits): |
|
|
"""Build dependency matrix for heatmap""" |
|
|
produced_list = sorted(list(produced_kits)) |
|
|
|
|
|
if len(produced_list) == 0: |
|
|
return pd.DataFrame() |
|
|
|
|
|
|
|
|
matrix = pd.DataFrame(0, index=produced_list, columns=produced_list) |
|
|
|
|
|
|
|
|
for kit_id in produced_list: |
|
|
kit_info = hierarchy_data.get(kit_id, {}) |
|
|
dependencies = kit_info.get('dependencies', []) |
|
|
|
|
|
for dep in dependencies: |
|
|
if dep in produced_list: |
|
|
matrix.loc[kit_id, dep] = 1 |
|
|
|
|
|
return matrix |
|
|
|
|
|
def get_production_timing(results): |
|
|
"""Extract production timing for each kit""" |
|
|
timing = {} |
|
|
|
|
|
if 'run_schedule' in results: |
|
|
for run in results['run_schedule']: |
|
|
kit = run['product'] |
|
|
day = run['day'] |
|
|
|
|
|
|
|
|
if kit not in timing or day < timing[kit]: |
|
|
timing[kit] = day |
|
|
|
|
|
return timing |
|
|
|
|
|
def create_interactive_network_graph(relationships, production_timing): |
|
|
"""Create interactive network graph using NetworkX layout""" |
|
|
if not NETWORKX_AVAILABLE: |
|
|
return create_simple_dependency_chart(relationships, production_timing) |
|
|
|
|
|
|
|
|
G = nx.DiGraph() |
|
|
|
|
|
|
|
|
for rel in relationships: |
|
|
G.add_edge(rel['source'], rel['target'], type=rel['type']) |
|
|
|
|
|
if len(G.nodes()) == 0: |
|
|
return go.Figure().add_annotation( |
|
|
text="No relationships to display", |
|
|
xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False |
|
|
) |
|
|
|
|
|
|
|
|
pos = nx.spring_layout(G, k=3, iterations=50) |
|
|
|
|
|
|
|
|
edge_x, edge_y = [], [] |
|
|
edge_info = [] |
|
|
|
|
|
for edge in G.edges(): |
|
|
source, target = edge |
|
|
x0, y0 = pos[source] |
|
|
x1, y1 = pos[target] |
|
|
|
|
|
edge_x.extend([x0, x1, None]) |
|
|
edge_y.extend([y0, y1, None]) |
|
|
|
|
|
|
|
|
edge_info.append({ |
|
|
'x': (x0 + x1) / 2, |
|
|
'y': (y0 + y1) / 2, |
|
|
'text': 'β', |
|
|
'source': source, |
|
|
'target': target |
|
|
}) |
|
|
|
|
|
edge_trace = go.Scatter(x=edge_x, y=edge_y, |
|
|
line=dict(width=2, color='#888'), |
|
|
hoverinfo='none', |
|
|
mode='lines') |
|
|
|
|
|
|
|
|
node_x, node_y, node_text, node_color, node_size = [], [], [], [], [] |
|
|
node_info = [] |
|
|
|
|
|
for node in G.nodes(): |
|
|
x, y = pos[node] |
|
|
node_x.append(x) |
|
|
node_y.append(y) |
|
|
|
|
|
|
|
|
in_degree = G.in_degree(node) |
|
|
out_degree = G.out_degree(node) |
|
|
total_degree = in_degree + out_degree |
|
|
node_size.append(20 + total_degree * 5) |
|
|
|
|
|
|
|
|
prod_day = production_timing.get(node, 0) |
|
|
if prod_day == 1: |
|
|
node_color.append('#90EE90') |
|
|
elif prod_day <= 3: |
|
|
node_color.append('#FFD700') |
|
|
else: |
|
|
node_color.append('#FF6347') |
|
|
|
|
|
|
|
|
short_name = node[:12] + "..." if len(node) > 12 else node |
|
|
node_text.append(short_name) |
|
|
|
|
|
node_info.append(f"{node}<br>Day: {prod_day}<br>In: {in_degree}, Out: {out_degree}") |
|
|
|
|
|
node_trace = go.Scatter(x=node_x, y=node_y, |
|
|
mode='markers+text', |
|
|
text=node_text, |
|
|
textposition='middle center', |
|
|
hovertext=node_info, |
|
|
hoverinfo='text', |
|
|
marker=dict(size=node_size, |
|
|
color=node_color, |
|
|
line=dict(width=2, color='black'))) |
|
|
|
|
|
|
|
|
fig = go.Figure(data=[edge_trace, node_trace], |
|
|
layout=go.Layout( |
|
|
title='Kit Dependency Network (Size=Connections, Color=Production Day)', |
|
|
showlegend=False, |
|
|
hovermode='closest', |
|
|
margin=dict(b=20,l=5,r=5,t=40), |
|
|
annotations=[ |
|
|
dict(text="Green=Early, Gold=Middle, Red=Late production", |
|
|
showarrow=False, |
|
|
xref="paper", yref="paper", |
|
|
x=0.005, y=-0.002, |
|
|
xanchor='left', yanchor='bottom', |
|
|
font=dict(size=12)) |
|
|
], |
|
|
xaxis=dict(showgrid=False, zeroline=False, showticklabels=False), |
|
|
yaxis=dict(showgrid=False, zeroline=False, showticklabels=False))) |
|
|
|
|
|
return fig |
|
|
|
|
|
def create_simple_dependency_chart(relationships, production_timing): |
|
|
"""Create simple dependency chart without NetworkX""" |
|
|
if not relationships: |
|
|
return go.Figure().add_annotation( |
|
|
text="No dependencies to display", |
|
|
xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
sources = set(rel['source'] for rel in relationships) |
|
|
targets = set(rel['target'] for rel in relationships) |
|
|
|
|
|
|
|
|
all_kits = list(sources | targets) |
|
|
positions = {kit: (i, production_timing.get(kit, 0)) for i, kit in enumerate(all_kits)} |
|
|
|
|
|
|
|
|
edge_x, edge_y = [], [] |
|
|
for rel in relationships: |
|
|
source_pos = positions[rel['source']] |
|
|
target_pos = positions[rel['target']] |
|
|
|
|
|
edge_x.extend([source_pos[0], target_pos[0], None]) |
|
|
edge_y.extend([source_pos[1], target_pos[1], None]) |
|
|
|
|
|
|
|
|
edge_trace = go.Scatter(x=edge_x, y=edge_y, |
|
|
line=dict(width=2, color='#888'), |
|
|
hoverinfo='none', |
|
|
mode='lines') |
|
|
|
|
|
|
|
|
node_x = [positions[kit][0] for kit in all_kits] |
|
|
node_y = [positions[kit][1] for kit in all_kits] |
|
|
node_text = [kit[:10] + "..." if len(kit) > 10 else kit for kit in all_kits] |
|
|
|
|
|
node_trace = go.Scatter(x=node_x, y=node_y, |
|
|
mode='markers+text', |
|
|
text=node_text, |
|
|
textposition='top center', |
|
|
marker=dict(size=15, color='lightblue', |
|
|
line=dict(width=2, color='black')), |
|
|
hovertext=all_kits, |
|
|
hoverinfo='text') |
|
|
|
|
|
fig = go.Figure(data=[edge_trace, node_trace], |
|
|
layout=go.Layout( |
|
|
title='Kit Dependencies (Y-axis = Production Day)', |
|
|
showlegend=False, |
|
|
xaxis=dict(title='Kits'), |
|
|
yaxis=dict(title='Production Day'))) |
|
|
|
|
|
return fig |
|
|
|
|
|
def create_production_timeline_with_dependencies(production_timing, relationships): |
|
|
"""Create timeline showing production order with dependency arrows""" |
|
|
if not production_timing: |
|
|
return go.Figure() |
|
|
|
|
|
|
|
|
timeline_data = [] |
|
|
for kit, day in production_timing.items(): |
|
|
timeline_data.append({ |
|
|
'Kit': kit, |
|
|
'Day': day, |
|
|
'Short_Name': kit[:15] + "..." if len(kit) > 15 else kit |
|
|
}) |
|
|
|
|
|
df = pd.DataFrame(timeline_data) |
|
|
|
|
|
|
|
|
fig = px.scatter(df, x='Day', y='Kit', |
|
|
hover_data=['Kit'], |
|
|
title='Production Timeline with Dependencies') |
|
|
|
|
|
|
|
|
for rel in relationships: |
|
|
source_day = production_timing.get(rel['source'], 0) |
|
|
target_day = production_timing.get(rel['target'], 0) |
|
|
|
|
|
|
|
|
if source_day > 0 and target_day > 0: |
|
|
fig.add_annotation( |
|
|
x=target_day, y=rel['target'], |
|
|
ax=source_day, ay=rel['source'], |
|
|
arrowhead=2, arrowsize=1, arrowwidth=2, |
|
|
arrowcolor="red" if source_day > target_day else "green" |
|
|
) |
|
|
|
|
|
fig.update_layout(height=max(400, len(df) * 20)) |
|
|
return fig |
|
|
|
|
|
def calculate_dependency_depth(relationships): |
|
|
"""Calculate maximum dependency depth""" |
|
|
if not NETWORKX_AVAILABLE or not relationships: |
|
|
return 0 |
|
|
|
|
|
G = nx.DiGraph() |
|
|
for rel in relationships: |
|
|
G.add_edge(rel['source'], rel['target']) |
|
|
|
|
|
try: |
|
|
return nx.dag_longest_path_length(G) |
|
|
except: |
|
|
return 0 |
|
|
|
|
|
def get_most_dependent_kits(relationships): |
|
|
"""Get kits with most dependencies""" |
|
|
dependency_counts = {} |
|
|
|
|
|
for rel in relationships: |
|
|
target = rel['target'] |
|
|
dependency_counts[target] = dependency_counts.get(target, 0) + 1 |
|
|
|
|
|
return sorted(dependency_counts.items(), key=lambda x: x[1], reverse=True) |
|
|
|
|
|
def find_dependency_violations(production_timing, relationships): |
|
|
"""Find cases where kits were produced before their dependencies""" |
|
|
violations = [] |
|
|
|
|
|
for rel in relationships: |
|
|
source = rel['source'] |
|
|
target = rel['target'] |
|
|
|
|
|
source_day = production_timing.get(source, 0) |
|
|
target_day = production_timing.get(target, 0) |
|
|
|
|
|
if source_day > 0 and target_day > 0 and source_day > target_day: |
|
|
days_early = source_day - target_day |
|
|
severity = 'high' if days_early > 2 else 'medium' if days_early > 1 else 'low' |
|
|
|
|
|
violations.append({ |
|
|
'kit': target, |
|
|
'dependency': source, |
|
|
'kit_day': target_day, |
|
|
'dependency_day': source_day, |
|
|
'days_early': days_early, |
|
|
'severity': severity |
|
|
}) |
|
|
|
|
|
return violations |
|
|
|
|
|
def analyze_dependency_timing(production_timing, relationships): |
|
|
"""Analyze timing of all dependency relationships""" |
|
|
timing_analysis = [] |
|
|
|
|
|
for rel in relationships: |
|
|
source = rel['source'] |
|
|
target = rel['target'] |
|
|
|
|
|
source_day = production_timing.get(source, 0) |
|
|
target_day = production_timing.get(target, 0) |
|
|
|
|
|
if source_day > 0 and target_day > 0: |
|
|
timing_diff = target_day - source_day |
|
|
status = "β
Correct" if timing_diff >= 0 else "β Violation" |
|
|
|
|
|
timing_analysis.append({ |
|
|
'Kit': target[:20] + "..." if len(target) > 20 else target, |
|
|
'Dependency': source[:20] + "..." if len(source) > 20 else source, |
|
|
'Kit Day': target_day, |
|
|
'Dep Day': source_day, |
|
|
'Gap (Days)': timing_diff, |
|
|
'Status': status |
|
|
}) |
|
|
|
|
|
return sorted(timing_analysis, key=lambda x: x['Gap (Days)']) |
|
|
|
|
|
def generate_dependency_recommendations(violations, relationships, production_timing): |
|
|
"""Generate recommendations based on dependency analysis""" |
|
|
recommendations = [] |
|
|
|
|
|
if not violations: |
|
|
recommendations.append("Excellent! All dependencies are being fulfilled in the correct order.") |
|
|
return recommendations |
|
|
|
|
|
|
|
|
high_severity = [v for v in violations if v['severity'] == 'high'] |
|
|
medium_severity = [v for v in violations if v['severity'] == 'medium'] |
|
|
|
|
|
if high_severity: |
|
|
recommendations.append( |
|
|
f"π¨ High Priority: {len(high_severity)} critical dependency violations found. " |
|
|
"Consider rescheduling production to ensure dependencies are produced first." |
|
|
) |
|
|
|
|
|
if medium_severity: |
|
|
recommendations.append( |
|
|
f"β οΈ Medium Priority: {len(medium_severity)} moderate dependency timing issues. " |
|
|
"Review production sequence for optimization opportunities." |
|
|
) |
|
|
|
|
|
|
|
|
problem_kits = {} |
|
|
for v in violations: |
|
|
kit = v['kit'] |
|
|
problem_kits[kit] = problem_kits.get(kit, 0) + 1 |
|
|
|
|
|
if problem_kits: |
|
|
worst_kit = max(problem_kits.items(), key=lambda x: x[1]) |
|
|
recommendations.append( |
|
|
f"π― Focus Area: Kit {worst_kit[0]} has {worst_kit[1]} dependency issues. " |
|
|
"Consider moving its production later in the schedule." |
|
|
) |
|
|
|
|
|
return recommendations |