hirann commited on
Commit
63ab4b1
·
verified ·
1 Parent(s): 0fb3128

Upload immunoorg/org_graph.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. immunoorg/org_graph.py +433 -433
immunoorg/org_graph.py CHANGED
@@ -1,433 +1,433 @@
1
- """
2
- Organizational Graph Engine
3
- ============================
4
- Simulates the company's departmental structure with communication channels,
5
- trust weights, KPI conflicts, and bureaucracy latencies.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- import random
11
- from typing import Any
12
-
13
- import networkx as nx
14
-
15
- from immunoorg.models import (
16
- DepartmentType, KPI, OrgEdge, OrgNode,
17
- )
18
-
19
-
20
- # Default department configurations
21
- DEPARTMENT_CONFIGS: dict[DepartmentType, dict[str, Any]] = {
22
- DepartmentType.IT_OPS: {
23
- "name": "IT Operations",
24
- "kpis": [
25
- KPI(name="system_uptime", target_value=99.9, current_value=99.5, weight=1.0, direction="maximize"),
26
- KPI(name="mttr", target_value=15.0, current_value=30.0, weight=0.7, direction="minimize"),
27
- ],
28
- "approval_authority": ["isolate_node", "restore_backup", "deploy_patch", "enable_ids"],
29
- "response_latency": 1.0,
30
- "cooperation_threshold": 0.4,
31
- "budget": 150.0,
32
- "headcount": 15,
33
- },
34
- DepartmentType.SECURITY: {
35
- "name": "Cybersecurity",
36
- "kpis": [
37
- KPI(name="threats_neutralized", target_value=100.0, current_value=85.0, weight=1.0, direction="maximize"),
38
- KPI(name="false_positive_rate", target_value=5.0, current_value=12.0, weight=0.8, direction="minimize"),
39
- ],
40
- "approval_authority": ["block_port", "quarantine_traffic", "rotate_credentials", "snapshot_forensics"],
41
- "response_latency": 0.5,
42
- "cooperation_threshold": 0.3,
43
- "budget": 200.0,
44
- "headcount": 12,
45
- },
46
- DepartmentType.ENGINEERING: {
47
- "name": "Software Engineering",
48
- "kpis": [
49
- KPI(name="feature_velocity", target_value=20.0, current_value=18.0, weight=1.0, direction="maximize"),
50
- KPI(name="deploy_frequency", target_value=10.0, current_value=8.0, weight=0.6, direction="maximize"),
51
- ],
52
- "approval_authority": ["deploy_patch"],
53
- "response_latency": 2.0,
54
- "cooperation_threshold": 0.6,
55
- "budget": 300.0,
56
- "headcount": 40,
57
- },
58
- DepartmentType.DEVOPS: {
59
- "name": "DevOps",
60
- "kpis": [
61
- KPI(name="deployment_speed", target_value=5.0, current_value=8.0, weight=1.0, direction="minimize"),
62
- KPI(name="pipeline_reliability", target_value=99.0, current_value=96.0, weight=0.8, direction="maximize"),
63
- ],
64
- "approval_authority": ["deploy_patch", "restore_backup", "isolate_node"],
65
- "response_latency": 1.0,
66
- "cooperation_threshold": 0.4,
67
- "budget": 120.0,
68
- "headcount": 10,
69
- },
70
- DepartmentType.MANAGEMENT: {
71
- "name": "Executive Management",
72
- "kpis": [
73
- KPI(name="cost_efficiency", target_value=0.8, current_value=0.7, weight=1.0, direction="maximize"),
74
- KPI(name="risk_score", target_value=0.2, current_value=0.4, weight=0.9, direction="minimize"),
75
- ],
76
- "approval_authority": [
77
- "merge_departments", "split_department", "reassign_authority",
78
- "rewrite_policy", "add_cross_functional_team",
79
- ],
80
- "response_latency": 3.0,
81
- "cooperation_threshold": 0.5,
82
- "budget": 500.0,
83
- "headcount": 5,
84
- },
85
- DepartmentType.LEGAL: {
86
- "name": "Legal & Compliance",
87
- "kpis": [
88
- KPI(name="compliance_score", target_value=100.0, current_value=92.0, weight=1.0, direction="maximize"),
89
- KPI(name="audit_readiness", target_value=1.0, current_value=0.8, weight=0.7, direction="maximize"),
90
- ],
91
- "approval_authority": ["rewrite_policy", "update_approval_protocol"],
92
- "response_latency": 4.0,
93
- "cooperation_threshold": 0.7,
94
- "budget": 80.0,
95
- "headcount": 8,
96
- },
97
- DepartmentType.HR: {
98
- "name": "Human Resources",
99
- "kpis": [
100
- KPI(name="employee_satisfaction", target_value=85.0, current_value=72.0, weight=1.0, direction="maximize"),
101
- KPI(name="turnover_rate", target_value=5.0, current_value=12.0, weight=0.8, direction="minimize"),
102
- ],
103
- "approval_authority": ["merge_departments", "split_department"],
104
- "response_latency": 3.0,
105
- "cooperation_threshold": 0.5,
106
- "budget": 90.0,
107
- "headcount": 8,
108
- },
109
- DepartmentType.FINANCE: {
110
- "name": "Finance",
111
- "kpis": [
112
- KPI(name="budget_utilization", target_value=0.9, current_value=0.85, weight=1.0, direction="maximize"),
113
- KPI(name="cost_overrun", target_value=0.0, current_value=0.05, weight=0.9, direction="minimize"),
114
- ],
115
- "approval_authority": ["update_approval_protocol"],
116
- "response_latency": 2.5,
117
- "cooperation_threshold": 0.6,
118
- "budget": 100.0,
119
- "headcount": 10,
120
- },
121
- }
122
-
123
-
124
- class OrgGraph:
125
- """Manages the organizational structure with departments, communication channels, and trust."""
126
-
127
- def __init__(self, difficulty: int = 1, seed: int | None = None):
128
- self.difficulty = difficulty
129
- self.rng = random.Random(seed)
130
- self.graph = nx.DiGraph()
131
- self.nodes: dict[str, OrgNode] = {}
132
- self.edges: list[OrgEdge] = []
133
- self._initial_edges_snapshot: list[OrgEdge] = []
134
-
135
- def generate_org_structure(self, network_node_ids: list[str]) -> None:
136
- """Generate the organizational structure and assign network nodes to departments."""
137
- # Departments to include based on difficulty
138
- dept_sets = {
139
- 1: [DepartmentType.IT_OPS, DepartmentType.SECURITY, DepartmentType.MANAGEMENT],
140
- 2: [DepartmentType.IT_OPS, DepartmentType.SECURITY, DepartmentType.ENGINEERING,
141
- DepartmentType.MANAGEMENT],
142
- 3: [DepartmentType.IT_OPS, DepartmentType.SECURITY, DepartmentType.ENGINEERING,
143
- DepartmentType.DEVOPS, DepartmentType.MANAGEMENT, DepartmentType.LEGAL],
144
- 4: list(DepartmentType), # All departments
145
- }
146
- active_depts = dept_sets.get(self.difficulty, dept_sets[1])
147
-
148
- # Create department nodes
149
- for dept_type in active_depts:
150
- config = DEPARTMENT_CONFIGS[dept_type]
151
- node = OrgNode(
152
- id=f"dept-{dept_type.value}",
153
- name=config["name"],
154
- department_type=dept_type,
155
- trust_score=0.7 + self.rng.uniform(-0.1, 0.1),
156
- response_latency=config["response_latency"] * (1.0 + (self.difficulty - 1) * 0.3),
157
- cooperation_threshold=config["cooperation_threshold"],
158
- kpis=config["kpis"],
159
- approval_authority=config["approval_authority"],
160
- budget=config["budget"],
161
- headcount=config["headcount"],
162
- )
163
- self.nodes[node.id] = node
164
- self.graph.add_node(node.id, dept_type=dept_type.value)
165
-
166
- # Assign network nodes to departments
167
- self._assign_network_nodes(network_node_ids)
168
-
169
- # Create communication channels
170
- self._create_channels()
171
- self._initial_edges_snapshot = [e.model_copy() for e in self.edges]
172
-
173
- def _assign_network_nodes(self, network_node_ids: list[str]) -> None:
174
- """Assign network nodes to departments based on tier mappings."""
175
- tier_dept_map = {
176
- "web": DepartmentType.ENGINEERING,
177
- "app": DepartmentType.ENGINEERING,
178
- "data": DepartmentType.IT_OPS,
179
- "management": DepartmentType.IT_OPS,
180
- "dmz": DepartmentType.SECURITY,
181
- }
182
- for net_id in network_node_ids:
183
- parts = net_id.split("-")
184
- tier = parts[0] if parts else "app"
185
- dept_type = tier_dept_map.get(tier, DepartmentType.IT_OPS)
186
- dept_id = f"dept-{dept_type.value}"
187
- if dept_id in self.nodes:
188
- self.nodes[dept_id].technical_nodes_owned.append(net_id)
189
- else:
190
- # Fallback to IT ops
191
- fallback = f"dept-{DepartmentType.IT_OPS.value}"
192
- if fallback in self.nodes:
193
- self.nodes[fallback].technical_nodes_owned.append(net_id)
194
-
195
- def _create_channels(self) -> None:
196
- """Create communication channels between departments."""
197
- node_ids = list(self.nodes.keys())
198
- # Standard channels based on organizational reality
199
- standard_channels = [
200
- (DepartmentType.IT_OPS, DepartmentType.SECURITY, 1.0, 0.7),
201
- (DepartmentType.IT_OPS, DepartmentType.DEVOPS, 0.5, 0.8),
202
- (DepartmentType.SECURITY, DepartmentType.MANAGEMENT, 2.0, 0.5),
203
- (DepartmentType.ENGINEERING, DepartmentType.DEVOPS, 0.5, 0.8),
204
- (DepartmentType.ENGINEERING, DepartmentType.MANAGEMENT, 2.5, 0.4),
205
- (DepartmentType.MANAGEMENT, DepartmentType.LEGAL, 1.5, 0.6),
206
- (DepartmentType.MANAGEMENT, DepartmentType.HR, 1.5, 0.6),
207
- (DepartmentType.MANAGEMENT, DepartmentType.FINANCE, 1.0, 0.7),
208
- (DepartmentType.LEGAL, DepartmentType.HR, 2.0, 0.5),
209
- ]
210
-
211
- for src_type, dst_type, base_latency, base_trust in standard_channels:
212
- src_id = f"dept-{src_type.value}"
213
- dst_id = f"dept-{dst_type.value}"
214
- if src_id in self.nodes and dst_id in self.nodes:
215
- latency = base_latency * (1.0 + (self.difficulty - 1) * 0.5)
216
- edge = OrgEdge(
217
- source=src_id, target=dst_id,
218
- latency=latency,
219
- trust=base_trust + self.rng.uniform(-0.1, 0.1),
220
- bandwidth=1.0,
221
- formal=True,
222
- )
223
- self.edges.append(edge)
224
- self.graph.add_edge(src_id, dst_id, weight=latency)
225
- # Add reverse edge (bidirectional communication) with slightly more latency
226
- rev_edge = OrgEdge(
227
- source=dst_id, target=src_id,
228
- latency=latency * 1.2,
229
- trust=base_trust + self.rng.uniform(-0.1, 0.1),
230
- bandwidth=1.0,
231
- formal=True,
232
- )
233
- self.edges.append(rev_edge)
234
- self.graph.add_edge(dst_id, src_id, weight=latency * 1.2)
235
-
236
- # At higher difficulty, intentionally create "silos" by removing some channels
237
- if self.difficulty >= 3:
238
- removable = [e for e in self.edges
239
- if "security" in e.source and "engineering" in e.target
240
- or "engineering" in e.source and "security" in e.target]
241
- for e in removable[:1]:
242
- e.active = False
243
-
244
- def get_node(self, node_id: str) -> OrgNode | None:
245
- return self.nodes.get(node_id)
246
-
247
- def get_all_nodes(self) -> list[OrgNode]:
248
- return list(self.nodes.values())
249
-
250
- def get_all_edges(self) -> list[OrgEdge]:
251
- return list(self.edges)
252
-
253
- def get_active_edges(self) -> list[OrgEdge]:
254
- return [e for e in self.edges if e.active]
255
-
256
- def find_approval_path(self, requester_id: str, action_name: str) -> list[str]:
257
- """Find the shortest approval path for an action through the org graph."""
258
- # Find which department can approve this action
259
- approvers = []
260
- for node in self.nodes.values():
261
- if action_name in node.approval_authority:
262
- approvers.append(node.id)
263
-
264
- if not approvers:
265
- return []
266
-
267
- # Build active-only graph
268
- active_graph = nx.DiGraph()
269
- for e in self.edges:
270
- if e.active:
271
- active_graph.add_edge(e.source, e.target, weight=e.latency)
272
-
273
- # Find shortest path to any approver
274
- best_path: list[str] = []
275
- best_cost = float("inf")
276
- for approver in approvers:
277
- try:
278
- path = nx.shortest_path(active_graph, requester_id, approver, weight="weight")
279
- cost = nx.shortest_path_length(active_graph, requester_id, approver, weight="weight")
280
- if cost < best_cost:
281
- best_cost = cost
282
- best_path = path
283
- except (nx.NetworkXNoPath, nx.NodeNotFound):
284
- continue
285
-
286
- return best_path
287
-
288
- def calculate_approval_latency(self, path: list[str]) -> float:
289
- """Calculate total latency for an approval path."""
290
- if len(path) < 2:
291
- return 0.0
292
- total = 0.0
293
- for i in range(len(path) - 1):
294
- for edge in self.edges:
295
- if edge.source == path[i] and edge.target == path[i + 1] and edge.active:
296
- total += edge.latency
297
- # Add node processing time
298
- node = self.nodes.get(path[i + 1])
299
- if node:
300
- total += node.response_latency
301
- break
302
- return total
303
-
304
- def merge_departments(self, dept_a_id: str, dept_b_id: str) -> OrgNode | None:
305
- """Merge two departments into one."""
306
- a = self.nodes.get(dept_a_id)
307
- b = self.nodes.get(dept_b_id)
308
- if not a or not b:
309
- return None
310
-
311
- merged = OrgNode(
312
- id=f"dept-merged-{a.department_type.value}-{b.department_type.value}",
313
- name=f"{a.name} + {b.name}",
314
- department_type=a.department_type,
315
- trust_score=(a.trust_score + b.trust_score) / 2,
316
- response_latency=min(a.response_latency, b.response_latency),
317
- cooperation_threshold=min(a.cooperation_threshold, b.cooperation_threshold),
318
- kpis=a.kpis + b.kpis,
319
- approval_authority=list(set(a.approval_authority + b.approval_authority)),
320
- budget=a.budget + b.budget,
321
- headcount=a.headcount + b.headcount,
322
- technical_nodes_owned=a.technical_nodes_owned + b.technical_nodes_owned,
323
- )
324
-
325
- # Deactivate old departments
326
- a.active = False
327
- b.active = False
328
-
329
- # Add merged dept
330
- self.nodes[merged.id] = merged
331
- self.graph.add_node(merged.id)
332
-
333
- # Rewire edges
334
- for edge in self.edges:
335
- if edge.source in (dept_a_id, dept_b_id):
336
- if edge.target not in (dept_a_id, dept_b_id):
337
- new_edge = OrgEdge(
338
- source=merged.id, target=edge.target,
339
- latency=edge.latency * 0.7, trust=edge.trust, formal=True,
340
- )
341
- self.edges.append(new_edge)
342
- self.graph.add_edge(merged.id, edge.target, weight=new_edge.latency)
343
- if edge.target in (dept_a_id, dept_b_id):
344
- if edge.source not in (dept_a_id, dept_b_id):
345
- new_edge = OrgEdge(
346
- source=edge.source, target=merged.id,
347
- latency=edge.latency * 0.7, trust=edge.trust, formal=True,
348
- )
349
- self.edges.append(new_edge)
350
- self.graph.add_edge(edge.source, merged.id, weight=new_edge.latency)
351
-
352
- return merged
353
-
354
- def create_shortcut_edge(self, src_id: str, dst_id: str) -> OrgEdge | None:
355
- """Create a new fast communication channel between departments."""
356
- if src_id not in self.nodes or dst_id not in self.nodes:
357
- return None
358
- edge = OrgEdge(
359
- source=src_id, target=dst_id,
360
- latency=0.5, trust=0.6, bandwidth=2.0,
361
- formal=False,
362
- )
363
- self.edges.append(edge)
364
- self.graph.add_edge(src_id, dst_id, weight=0.5)
365
- return edge
366
-
367
- def reduce_bureaucracy(self, dept_id: str) -> bool:
368
- """Reduce latency on all edges connected to a department."""
369
- node = self.nodes.get(dept_id)
370
- if not node:
371
- return False
372
- node.response_latency *= 0.6
373
- for edge in self.edges:
374
- if edge.source == dept_id or edge.target == dept_id:
375
- edge.latency *= 0.7
376
- return True
377
-
378
- def update_approval_protocol(self, dept_id: str, new_authorities: list[str]) -> bool:
379
- """Update what a department can approve."""
380
- node = self.nodes.get(dept_id)
381
- if not node:
382
- return False
383
- node.approval_authority = list(set(node.approval_authority + new_authorities))
384
- return True
385
-
386
- def calculate_org_efficiency(self) -> float:
387
- """Calculate overall organizational efficiency (0-1). Higher = better."""
388
- if not self.nodes:
389
- return 0.0
390
-
391
- active_nodes = [n for n in self.nodes.values() if n.active]
392
- if not active_nodes:
393
- return 0.0
394
-
395
- avg_latency = sum(n.response_latency for n in active_nodes) / len(active_nodes)
396
- avg_trust = sum(n.trust_score for n in active_nodes) / len(active_nodes)
397
-
398
- active_edges = self.get_active_edges()
399
- connectivity = len(active_edges) / max(1, len(active_nodes) * (len(active_nodes) - 1))
400
-
401
- # Efficiency: high trust, low latency, good connectivity
402
- latency_score = max(0.0, 1.0 - avg_latency / 10.0)
403
- efficiency = (avg_trust * 0.4 + latency_score * 0.4 + min(1.0, connectivity * 2) * 0.2)
404
- return min(1.0, max(0.0, efficiency))
405
-
406
- def calculate_org_chaos(self) -> float:
407
- """Calculate how much the org has changed from initial state (0=unchanged, 1=total chaos)."""
408
- if not self._initial_edges_snapshot:
409
- return 0.0
410
- initial_set = {(e.source, e.target) for e in self._initial_edges_snapshot}
411
- current_set = {(e.source, e.target) for e in self.edges if e.active}
412
- added = current_set - initial_set
413
- removed = initial_set - current_set
414
- total_changes = len(added) + len(removed)
415
- max_possible = max(1, len(initial_set) * 2)
416
- return min(1.0, total_changes / max_possible)
417
-
418
- def identify_silos(self) -> list[tuple[str, str]]:
419
- """Identify department pairs that should be connected but aren't."""
420
- silos = []
421
- critical_pairs = [
422
- (DepartmentType.SECURITY, DepartmentType.ENGINEERING),
423
- (DepartmentType.SECURITY, DepartmentType.DEVOPS),
424
- (DepartmentType.IT_OPS, DepartmentType.ENGINEERING),
425
- ]
426
- active_edges_set = {(e.source, e.target) for e in self.edges if e.active}
427
- for dept_a, dept_b in critical_pairs:
428
- id_a = f"dept-{dept_a.value}"
429
- id_b = f"dept-{dept_b.value}"
430
- if id_a in self.nodes and id_b in self.nodes:
431
- if (id_a, id_b) not in active_edges_set and (id_b, id_a) not in active_edges_set:
432
- silos.append((id_a, id_b))
433
- return silos
 
1
+ """
2
+ Organizational Graph Engine
3
+ ============================
4
+ Simulates the company's departmental structure with communication channels,
5
+ trust weights, KPI conflicts, and bureaucracy latencies.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import random
11
+ from typing import Any
12
+
13
+ import networkx as nx
14
+
15
+ from immunoorg.models import (
16
+ DepartmentType, KPI, OrgEdge, OrgNode,
17
+ )
18
+
19
+
20
+ # Default department configurations
21
+ DEPARTMENT_CONFIGS: dict[DepartmentType, dict[str, Any]] = {
22
+ DepartmentType.IT_OPS: {
23
+ "name": "IT Operations",
24
+ "kpis": [
25
+ KPI(name="system_uptime", target_value=99.9, current_value=99.5, weight=1.0, direction="maximize"),
26
+ KPI(name="mttr", target_value=15.0, current_value=30.0, weight=0.7, direction="minimize"),
27
+ ],
28
+ "approval_authority": ["isolate_node", "restore_backup", "deploy_patch", "enable_ids"],
29
+ "response_latency": 1.0,
30
+ "cooperation_threshold": 0.4,
31
+ "budget": 150.0,
32
+ "headcount": 15,
33
+ },
34
+ DepartmentType.SECURITY: {
35
+ "name": "Cybersecurity",
36
+ "kpis": [
37
+ KPI(name="threats_neutralized", target_value=100.0, current_value=85.0, weight=1.0, direction="maximize"),
38
+ KPI(name="false_positive_rate", target_value=5.0, current_value=12.0, weight=0.8, direction="minimize"),
39
+ ],
40
+ "approval_authority": ["block_port", "quarantine_traffic", "rotate_credentials", "snapshot_forensics"],
41
+ "response_latency": 0.5,
42
+ "cooperation_threshold": 0.3,
43
+ "budget": 200.0,
44
+ "headcount": 12,
45
+ },
46
+ DepartmentType.ENGINEERING: {
47
+ "name": "Software Engineering",
48
+ "kpis": [
49
+ KPI(name="feature_velocity", target_value=20.0, current_value=18.0, weight=1.0, direction="maximize"),
50
+ KPI(name="deploy_frequency", target_value=10.0, current_value=8.0, weight=0.6, direction="maximize"),
51
+ ],
52
+ "approval_authority": ["deploy_patch"],
53
+ "response_latency": 2.0,
54
+ "cooperation_threshold": 0.6,
55
+ "budget": 300.0,
56
+ "headcount": 40,
57
+ },
58
+ DepartmentType.DEVOPS: {
59
+ "name": "DevOps",
60
+ "kpis": [
61
+ KPI(name="deployment_speed", target_value=5.0, current_value=8.0, weight=1.0, direction="minimize"),
62
+ KPI(name="pipeline_reliability", target_value=99.0, current_value=96.0, weight=0.8, direction="maximize"),
63
+ ],
64
+ "approval_authority": ["deploy_patch", "restore_backup", "isolate_node"],
65
+ "response_latency": 1.0,
66
+ "cooperation_threshold": 0.4,
67
+ "budget": 120.0,
68
+ "headcount": 10,
69
+ },
70
+ DepartmentType.MANAGEMENT: {
71
+ "name": "Executive Management",
72
+ "kpis": [
73
+ KPI(name="cost_efficiency", target_value=0.8, current_value=0.7, weight=1.0, direction="maximize"),
74
+ KPI(name="risk_score", target_value=0.2, current_value=0.4, weight=0.9, direction="minimize"),
75
+ ],
76
+ "approval_authority": [
77
+ "merge_departments", "split_department", "reassign_authority",
78
+ "rewrite_policy", "add_cross_functional_team",
79
+ ],
80
+ "response_latency": 3.0,
81
+ "cooperation_threshold": 0.5,
82
+ "budget": 500.0,
83
+ "headcount": 5,
84
+ },
85
+ DepartmentType.LEGAL: {
86
+ "name": "Legal & Compliance",
87
+ "kpis": [
88
+ KPI(name="compliance_score", target_value=100.0, current_value=92.0, weight=1.0, direction="maximize"),
89
+ KPI(name="audit_readiness", target_value=1.0, current_value=0.8, weight=0.7, direction="maximize"),
90
+ ],
91
+ "approval_authority": ["rewrite_policy", "update_approval_protocol"],
92
+ "response_latency": 4.0,
93
+ "cooperation_threshold": 0.7,
94
+ "budget": 80.0,
95
+ "headcount": 8,
96
+ },
97
+ DepartmentType.HR: {
98
+ "name": "Human Resources",
99
+ "kpis": [
100
+ KPI(name="employee_satisfaction", target_value=85.0, current_value=72.0, weight=1.0, direction="maximize"),
101
+ KPI(name="turnover_rate", target_value=5.0, current_value=12.0, weight=0.8, direction="minimize"),
102
+ ],
103
+ "approval_authority": ["merge_departments", "split_department"],
104
+ "response_latency": 3.0,
105
+ "cooperation_threshold": 0.5,
106
+ "budget": 90.0,
107
+ "headcount": 8,
108
+ },
109
+ DepartmentType.FINANCE: {
110
+ "name": "Finance",
111
+ "kpis": [
112
+ KPI(name="budget_utilization", target_value=0.9, current_value=0.85, weight=1.0, direction="maximize"),
113
+ KPI(name="cost_overrun", target_value=0.0, current_value=0.05, weight=0.9, direction="minimize"),
114
+ ],
115
+ "approval_authority": ["update_approval_protocol"],
116
+ "response_latency": 2.5,
117
+ "cooperation_threshold": 0.6,
118
+ "budget": 100.0,
119
+ "headcount": 10,
120
+ },
121
+ }
122
+
123
+
124
+ class OrgGraph:
125
+ """Manages the organizational structure with departments, communication channels, and trust."""
126
+
127
+ def __init__(self, difficulty: int = 1, seed: int | None = None):
128
+ self.difficulty = difficulty
129
+ self.rng = random.Random(seed)
130
+ self.graph = nx.DiGraph()
131
+ self.nodes: dict[str, OrgNode] = {}
132
+ self.edges: list[OrgEdge] = []
133
+ self._initial_edges_snapshot: list[OrgEdge] = []
134
+
135
+ def generate_org_structure(self, network_node_ids: list[str]) -> None:
136
+ """Generate the organizational structure and assign network nodes to departments."""
137
+ # Departments to include based on difficulty
138
+ dept_sets = {
139
+ 1: [DepartmentType.IT_OPS, DepartmentType.SECURITY, DepartmentType.MANAGEMENT],
140
+ 2: [DepartmentType.IT_OPS, DepartmentType.SECURITY, DepartmentType.ENGINEERING,
141
+ DepartmentType.MANAGEMENT],
142
+ 3: [DepartmentType.IT_OPS, DepartmentType.SECURITY, DepartmentType.ENGINEERING,
143
+ DepartmentType.DEVOPS, DepartmentType.MANAGEMENT, DepartmentType.LEGAL],
144
+ 4: list(DepartmentType), # All departments
145
+ }
146
+ active_depts = dept_sets.get(self.difficulty, dept_sets[1])
147
+
148
+ # Create department nodes
149
+ for dept_type in active_depts:
150
+ config = DEPARTMENT_CONFIGS[dept_type]
151
+ node = OrgNode(
152
+ id=f"dept-{dept_type.value}",
153
+ name=config["name"],
154
+ department_type=dept_type,
155
+ trust_score=0.7 + self.rng.uniform(-0.1, 0.1),
156
+ response_latency=config["response_latency"] * (1.0 + (self.difficulty - 1) * 0.3),
157
+ cooperation_threshold=config["cooperation_threshold"],
158
+ kpis=config["kpis"],
159
+ approval_authority=config["approval_authority"],
160
+ budget=config["budget"],
161
+ headcount=config["headcount"],
162
+ )
163
+ self.nodes[node.id] = node
164
+ self.graph.add_node(node.id, dept_type=dept_type.value)
165
+
166
+ # Assign network nodes to departments
167
+ self._assign_network_nodes(network_node_ids)
168
+
169
+ # Create communication channels
170
+ self._create_channels()
171
+ self._initial_edges_snapshot = [e.model_copy() for e in self.edges]
172
+
173
+ def _assign_network_nodes(self, network_node_ids: list[str]) -> None:
174
+ """Assign network nodes to departments based on tier mappings."""
175
+ tier_dept_map = {
176
+ "web": DepartmentType.ENGINEERING,
177
+ "app": DepartmentType.ENGINEERING,
178
+ "data": DepartmentType.IT_OPS,
179
+ "management": DepartmentType.IT_OPS,
180
+ "dmz": DepartmentType.SECURITY,
181
+ }
182
+ for net_id in network_node_ids:
183
+ parts = net_id.split("-")
184
+ tier = parts[0] if parts else "app"
185
+ dept_type = tier_dept_map.get(tier, DepartmentType.IT_OPS)
186
+ dept_id = f"dept-{dept_type.value}"
187
+ if dept_id in self.nodes:
188
+ self.nodes[dept_id].technical_nodes_owned.append(net_id)
189
+ else:
190
+ # Fallback to IT ops
191
+ fallback = f"dept-{DepartmentType.IT_OPS.value}"
192
+ if fallback in self.nodes:
193
+ self.nodes[fallback].technical_nodes_owned.append(net_id)
194
+
195
+ def _create_channels(self) -> None:
196
+ """Create communication channels between departments."""
197
+ node_ids = list(self.nodes.keys())
198
+ # Standard channels based on organizational reality
199
+ standard_channels = [
200
+ (DepartmentType.IT_OPS, DepartmentType.SECURITY, 1.0, 0.7),
201
+ (DepartmentType.IT_OPS, DepartmentType.DEVOPS, 0.5, 0.8),
202
+ (DepartmentType.SECURITY, DepartmentType.MANAGEMENT, 2.0, 0.5),
203
+ (DepartmentType.ENGINEERING, DepartmentType.DEVOPS, 0.5, 0.8),
204
+ (DepartmentType.ENGINEERING, DepartmentType.MANAGEMENT, 2.5, 0.4),
205
+ (DepartmentType.MANAGEMENT, DepartmentType.LEGAL, 1.5, 0.6),
206
+ (DepartmentType.MANAGEMENT, DepartmentType.HR, 1.5, 0.6),
207
+ (DepartmentType.MANAGEMENT, DepartmentType.FINANCE, 1.0, 0.7),
208
+ (DepartmentType.LEGAL, DepartmentType.HR, 2.0, 0.5),
209
+ ]
210
+
211
+ for src_type, dst_type, base_latency, base_trust in standard_channels:
212
+ src_id = f"dept-{src_type.value}"
213
+ dst_id = f"dept-{dst_type.value}"
214
+ if src_id in self.nodes and dst_id in self.nodes:
215
+ latency = base_latency * (1.0 + (self.difficulty - 1) * 0.5)
216
+ edge = OrgEdge(
217
+ source=src_id, target=dst_id,
218
+ latency=latency,
219
+ trust=base_trust + self.rng.uniform(-0.1, 0.1),
220
+ bandwidth=1.0,
221
+ formal=True,
222
+ )
223
+ self.edges.append(edge)
224
+ self.graph.add_edge(src_id, dst_id, weight=latency)
225
+ # Add reverse edge (bidirectional communication) with slightly more latency
226
+ rev_edge = OrgEdge(
227
+ source=dst_id, target=src_id,
228
+ latency=latency * 1.2,
229
+ trust=base_trust + self.rng.uniform(-0.1, 0.1),
230
+ bandwidth=1.0,
231
+ formal=True,
232
+ )
233
+ self.edges.append(rev_edge)
234
+ self.graph.add_edge(dst_id, src_id, weight=latency * 1.2)
235
+
236
+ # At higher difficulty, intentionally create "silos" by removing some channels
237
+ if self.difficulty >= 3:
238
+ removable = [e for e in self.edges
239
+ if "security" in e.source and "engineering" in e.target
240
+ or "engineering" in e.source and "security" in e.target]
241
+ for e in removable[:1]:
242
+ e.active = False
243
+
244
+ def get_node(self, node_id: str) -> OrgNode | None:
245
+ return self.nodes.get(node_id)
246
+
247
+ def get_all_nodes(self) -> list[OrgNode]:
248
+ return list(self.nodes.values())
249
+
250
+ def get_all_edges(self) -> list[OrgEdge]:
251
+ return list(self.edges)
252
+
253
+ def get_active_edges(self) -> list[OrgEdge]:
254
+ return [e for e in self.edges if e.active]
255
+
256
+ def find_approval_path(self, requester_id: str, action_name: str) -> list[str]:
257
+ """Find the shortest approval path for an action through the org graph."""
258
+ # Find which department can approve this action
259
+ approvers = []
260
+ for node in self.nodes.values():
261
+ if action_name in node.approval_authority:
262
+ approvers.append(node.id)
263
+
264
+ if not approvers:
265
+ return []
266
+
267
+ # Build active-only graph
268
+ active_graph = nx.DiGraph()
269
+ for e in self.edges:
270
+ if e.active:
271
+ active_graph.add_edge(e.source, e.target, weight=e.latency)
272
+
273
+ # Find shortest path to any approver
274
+ best_path: list[str] = []
275
+ best_cost = float("inf")
276
+ for approver in approvers:
277
+ try:
278
+ path = nx.shortest_path(active_graph, requester_id, approver, weight="weight")
279
+ cost = nx.shortest_path_length(active_graph, requester_id, approver, weight="weight")
280
+ if cost < best_cost:
281
+ best_cost = cost
282
+ best_path = path
283
+ except (nx.NetworkXNoPath, nx.NodeNotFound):
284
+ continue
285
+
286
+ return best_path
287
+
288
+ def calculate_approval_latency(self, path: list[str]) -> float:
289
+ """Calculate total latency for an approval path."""
290
+ if len(path) < 2:
291
+ return 0.0
292
+ total = 0.0
293
+ for i in range(len(path) - 1):
294
+ for edge in self.edges:
295
+ if edge.source == path[i] and edge.target == path[i + 1] and edge.active:
296
+ total += edge.latency
297
+ # Add node processing time
298
+ node = self.nodes.get(path[i + 1])
299
+ if node:
300
+ total += node.response_latency
301
+ break
302
+ return total
303
+
304
+ def merge_departments(self, dept_a_id: str, dept_b_id: str) -> OrgNode | None:
305
+ """Merge two departments into one."""
306
+ a = self.nodes.get(dept_a_id)
307
+ b = self.nodes.get(dept_b_id)
308
+ if not a or not b:
309
+ return None
310
+
311
+ merged = OrgNode(
312
+ id=f"dept-merged-{a.department_type.value}-{b.department_type.value}",
313
+ name=f"{a.name} + {b.name}",
314
+ department_type=a.department_type,
315
+ trust_score=(a.trust_score + b.trust_score) / 2,
316
+ response_latency=min(a.response_latency, b.response_latency),
317
+ cooperation_threshold=min(a.cooperation_threshold, b.cooperation_threshold),
318
+ kpis=a.kpis + b.kpis,
319
+ approval_authority=list(set(a.approval_authority + b.approval_authority)),
320
+ budget=a.budget + b.budget,
321
+ headcount=a.headcount + b.headcount,
322
+ technical_nodes_owned=a.technical_nodes_owned + b.technical_nodes_owned,
323
+ )
324
+
325
+ # Deactivate old departments
326
+ a.active = False
327
+ b.active = False
328
+
329
+ # Add merged dept
330
+ self.nodes[merged.id] = merged
331
+ self.graph.add_node(merged.id)
332
+
333
+ # Rewire edges
334
+ for edge in self.edges:
335
+ if edge.source in (dept_a_id, dept_b_id):
336
+ if edge.target not in (dept_a_id, dept_b_id):
337
+ new_edge = OrgEdge(
338
+ source=merged.id, target=edge.target,
339
+ latency=edge.latency * 0.7, trust=edge.trust, formal=True,
340
+ )
341
+ self.edges.append(new_edge)
342
+ self.graph.add_edge(merged.id, edge.target, weight=new_edge.latency)
343
+ if edge.target in (dept_a_id, dept_b_id):
344
+ if edge.source not in (dept_a_id, dept_b_id):
345
+ new_edge = OrgEdge(
346
+ source=edge.source, target=merged.id,
347
+ latency=edge.latency * 0.7, trust=edge.trust, formal=True,
348
+ )
349
+ self.edges.append(new_edge)
350
+ self.graph.add_edge(edge.source, merged.id, weight=new_edge.latency)
351
+
352
+ return merged
353
+
354
+ def create_shortcut_edge(self, src_id: str, dst_id: str) -> OrgEdge | None:
355
+ """Create a new fast communication channel between departments."""
356
+ if src_id not in self.nodes or dst_id not in self.nodes:
357
+ return None
358
+ edge = OrgEdge(
359
+ source=src_id, target=dst_id,
360
+ latency=0.5, trust=0.6, bandwidth=2.0,
361
+ formal=False,
362
+ )
363
+ self.edges.append(edge)
364
+ self.graph.add_edge(src_id, dst_id, weight=0.5)
365
+ return edge
366
+
367
+ def reduce_bureaucracy(self, dept_id: str) -> bool:
368
+ """Reduce latency on all edges connected to a department."""
369
+ node = self.nodes.get(dept_id)
370
+ if not node:
371
+ return False
372
+ node.response_latency *= 0.6
373
+ for edge in self.edges:
374
+ if edge.source == dept_id or edge.target == dept_id:
375
+ edge.latency *= 0.7
376
+ return True
377
+
378
+ def update_approval_protocol(self, dept_id: str, new_authorities: list[str]) -> bool:
379
+ """Update what a department can approve."""
380
+ node = self.nodes.get(dept_id)
381
+ if not node:
382
+ return False
383
+ node.approval_authority = list(set(node.approval_authority + new_authorities))
384
+ return True
385
+
386
+ def calculate_org_efficiency(self) -> float:
387
+ """Calculate overall organizational efficiency (0-1). Higher = better."""
388
+ if not self.nodes:
389
+ return 0.0
390
+
391
+ active_nodes = [n for n in self.nodes.values() if n.active]
392
+ if not active_nodes:
393
+ return 0.0
394
+
395
+ avg_latency = sum(n.response_latency for n in active_nodes) / len(active_nodes)
396
+ avg_trust = sum(n.trust_score for n in active_nodes) / len(active_nodes)
397
+
398
+ active_edges = self.get_active_edges()
399
+ connectivity = len(active_edges) / max(1, len(active_nodes) * (len(active_nodes) - 1))
400
+
401
+ # Efficiency: high trust, low latency, good connectivity
402
+ latency_score = max(0.0, 1.0 - avg_latency / 10.0)
403
+ efficiency = (avg_trust * 0.4 + latency_score * 0.4 + min(1.0, connectivity * 2) * 0.2)
404
+ return min(1.0, max(0.0, efficiency))
405
+
406
+ def calculate_org_chaos(self) -> float:
407
+ """Calculate how much the org has changed from initial state (0=unchanged, 1=total chaos)."""
408
+ if not self._initial_edges_snapshot:
409
+ return 0.0
410
+ initial_set = {(e.source, e.target) for e in self._initial_edges_snapshot}
411
+ current_set = {(e.source, e.target) for e in self.edges if e.active}
412
+ added = current_set - initial_set
413
+ removed = initial_set - current_set
414
+ total_changes = len(added) + len(removed)
415
+ max_possible = max(1, len(initial_set) * 2)
416
+ return min(1.0, total_changes / max_possible)
417
+
418
+ def identify_silos(self) -> list[tuple[str, str]]:
419
+ """Identify department pairs that should be connected but aren't."""
420
+ silos = []
421
+ critical_pairs = [
422
+ (DepartmentType.SECURITY, DepartmentType.ENGINEERING),
423
+ (DepartmentType.SECURITY, DepartmentType.DEVOPS),
424
+ (DepartmentType.IT_OPS, DepartmentType.ENGINEERING),
425
+ ]
426
+ active_edges_set = {(e.source, e.target) for e in self.edges if e.active}
427
+ for dept_a, dept_b in critical_pairs:
428
+ id_a = f"dept-{dept_a.value}"
429
+ id_b = f"dept-{dept_b.value}"
430
+ if id_a in self.nodes and id_b in self.nodes:
431
+ if (id_a, id_b) not in active_edges_set and (id_b, id_a) not in active_edges_set:
432
+ silos.append((id_a, id_b))
433
+ return silos