continuum-toolkit 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. cli/__init__.py +12 -0
  2. cli/main.py +415 -0
  3. confidence/__init__.py +13 -0
  4. confidence/calculator.py +308 -0
  5. confidence/models.py +40 -0
  6. context/__init__.py +17 -0
  7. context/models.py +108 -0
  8. context/pruner.py +228 -0
  9. context/selector.py +193 -0
  10. continuum_toolkit-1.0.0.dist-info/METADATA +511 -0
  11. continuum_toolkit-1.0.0.dist-info/RECORD +72 -0
  12. continuum_toolkit-1.0.0.dist-info/WHEEL +5 -0
  13. continuum_toolkit-1.0.0.dist-info/entry_points.txt +2 -0
  14. continuum_toolkit-1.0.0.dist-info/licenses/LICENSE +21 -0
  15. continuum_toolkit-1.0.0.dist-info/top_level.txt +13 -0
  16. contradictions/__init__.py +19 -0
  17. contradictions/detector.py +442 -0
  18. contradictions/models.py +75 -0
  19. core/__init__.py +97 -0
  20. core/enums.py +130 -0
  21. core/evidence.py +117 -0
  22. core/interfaces.py +209 -0
  23. core/schema.py +391 -0
  24. core/serializer.py +84 -0
  25. core/state_models.py +530 -0
  26. daemon/__init__.py +12 -0
  27. daemon/service.py +170 -0
  28. extractors/__init__.py +51 -0
  29. extractors/base.py +117 -0
  30. extractors/config/parsers.py +288 -0
  31. extractors/config/secret_sanitizer.py +91 -0
  32. extractors/config_extractor.py +172 -0
  33. extractors/conversation/analyzers.py +193 -0
  34. extractors/conversation/models.py +148 -0
  35. extractors/conversation_extractor.py +166 -0
  36. extractors/git_extractor.py +305 -0
  37. extractors/parsers/base.py +91 -0
  38. extractors/parsers/comment_parser.py +51 -0
  39. extractors/parsers/js_ts_parser.py +171 -0
  40. extractors/parsers/python_parser.py +180 -0
  41. extractors/verification/runners.py +278 -0
  42. extractors/verification_extractor.py +263 -0
  43. extractors/workspace_extractor.py +221 -0
  44. graph/__init__.py +24 -0
  45. graph/diff.py +109 -0
  46. graph/manager.py +473 -0
  47. graph/models.py +62 -0
  48. graph/propagator.py +194 -0
  49. graph/query.py +86 -0
  50. graph/snapshot.py +85 -0
  51. handoff/__init__.py +28 -0
  52. handoff/adapters/__init__.py +45 -0
  53. handoff/adapters/base.py +90 -0
  54. handoff/adapters/claude_adapter.py +176 -0
  55. handoff/adapters/codex_gpt_adapter.py +151 -0
  56. handoff/adapters/gemini_adapter.py +151 -0
  57. handoff/adapters/local_model_adapter.py +130 -0
  58. handoff/models.py +88 -0
  59. handoff/packager.py +288 -0
  60. pipeline/__init__.py +9 -0
  61. pipeline/orchestrator.py +260 -0
  62. resolution/__init__.py +15 -0
  63. resolution/resolver.py +311 -0
  64. storage/__init__.py +18 -0
  65. storage/hooks.py +125 -0
  66. storage/manager.py +127 -0
  67. storage/models.py +39 -0
  68. storage/recovery.py +98 -0
  69. watcher/__init__.py +17 -0
  70. watcher/detector.py +136 -0
  71. watcher/models.py +62 -0
  72. watcher/updater.py +163 -0
graph/manager.py ADDED
@@ -0,0 +1,473 @@
1
+ """
2
+ Project Continuum - State Graph (DAG) Manager
3
+ =============================================
4
+ Milestone 4 - Phase 9: State Graph (DAG) Construction.
5
+ Builds, validates, queries, and serializes the Canonical State Graph (DAG),
6
+ connecting milestones, phases, requirements, services, AST symbols, and tests
7
+ to their supporting physical evidence.
8
+ """
9
+
10
+ from collections import defaultdict, deque
11
+ from typing import Any, Dict, List, Optional, Set, Tuple
12
+
13
+ from core.enums import NodeType, RelationType, Status
14
+ from core.interfaces import IStateGraphManager
15
+ from core.state_models import (
16
+ CanonicalProjectState,
17
+ GraphEdge,
18
+ GraphNode,
19
+ )
20
+ from graph.models import GraphStats, GraphValidationResult
21
+
22
+
23
+ class StateGraphManager(IStateGraphManager):
24
+ """
25
+ Manages the Directed Acyclic Graph (DAG) of Project Continuum.
26
+ Implements IStateGraphManager Protocol.
27
+ """
28
+
29
+ def __init__(self, canonical_state: Optional[CanonicalProjectState] = None):
30
+ self._canonical_state: Optional[CanonicalProjectState] = canonical_state
31
+ self._nodes: Dict[str, GraphNode] = {}
32
+ self._edges: List[GraphEdge] = []
33
+
34
+ # Adjacency indexes for high-speed deterministic traversal
35
+ self._out_edges: Dict[str, List[GraphEdge]] = defaultdict(list)
36
+ self._in_edges: Dict[str, List[GraphEdge]] = defaultdict(list)
37
+
38
+ if canonical_state:
39
+ for node in canonical_state.graph_nodes.values():
40
+ self.add_node(node)
41
+ for edge in canonical_state.graph_edges:
42
+ self.add_edge(edge)
43
+
44
+ @property
45
+ def nodes(self) -> Dict[str, GraphNode]:
46
+ return self._nodes
47
+
48
+ @property
49
+ def edges(self) -> List[GraphEdge]:
50
+ return self._edges
51
+
52
+ def add_node(self, node: GraphNode) -> None:
53
+ """
54
+ Adds or updates a GraphNode deterministically.
55
+ """
56
+ self._nodes[node.id] = node
57
+ if self._canonical_state:
58
+ self._canonical_state.graph_nodes[node.id] = node
59
+
60
+ def get_node(self, node_id: str) -> Optional[GraphNode]:
61
+ """Retrieves a node by its unique ID."""
62
+ return self._nodes.get(node_id)
63
+
64
+ def add_edge(self, edge: GraphEdge, allow_cycle: bool = False) -> bool:
65
+ """
66
+ Adds a directed relationship between two existing nodes.
67
+ If allow_cycle is False and the new edge induces a cycle, the edge is rejected.
68
+ """
69
+ # Prevent duplicate identical edges
70
+ for existing in self._out_edges[edge.source_id]:
71
+ if existing.target_id == edge.target_id and existing.relation == edge.relation:
72
+ return True
73
+
74
+ self._edges.append(edge)
75
+ self._out_edges[edge.source_id].append(edge)
76
+ self._in_edges[edge.target_id].append(edge)
77
+
78
+ if not allow_cycle:
79
+ cycles = self.detect_cycles()
80
+ if cycles:
81
+ # Rollback edge insertion
82
+ self._edges.remove(edge)
83
+ self._out_edges[edge.source_id].remove(edge)
84
+ self._in_edges[edge.target_id].remove(edge)
85
+ return False
86
+
87
+ if self._canonical_state:
88
+ if edge not in self._canonical_state.graph_edges:
89
+ self._canonical_state.graph_edges.append(edge)
90
+
91
+ return True
92
+
93
+ def get_dependencies(self, node_id: str, recursive: bool = False) -> List[GraphNode]:
94
+ """
95
+ Retrieves nodes that `node_id` depends on / requires / implements / verifies.
96
+ (Follows outgoing DEPENDS_ON, REQUIRES, IMPLEMENTS, IMPORTS, VERIFIES edges).
97
+ """
98
+ dependency_relations = {
99
+ RelationType.DEPENDS_ON,
100
+ RelationType.REQUIRES,
101
+ RelationType.IMPLEMENTS,
102
+ RelationType.IMPORTS,
103
+ RelationType.VERIFIES,
104
+ }
105
+
106
+ visited: Set[str] = set()
107
+ queue: deque = deque()
108
+
109
+ for edge in self._out_edges.get(node_id, []):
110
+ if edge.relation in dependency_relations and edge.target_id in self._nodes:
111
+ queue.append(edge.target_id)
112
+ visited.add(edge.target_id)
113
+
114
+ results: List[GraphNode] = []
115
+
116
+ while queue:
117
+ curr_id = queue.popleft()
118
+ node = self._nodes.get(curr_id)
119
+ if node:
120
+ results.append(node)
121
+
122
+ if recursive:
123
+ for edge in self._out_edges.get(curr_id, []):
124
+ if edge.relation in dependency_relations:
125
+ if edge.target_id not in visited and edge.target_id in self._nodes:
126
+ visited.add(edge.target_id)
127
+ queue.append(edge.target_id)
128
+
129
+ return results
130
+
131
+ def get_dependents(self, node_id: str, recursive: bool = False) -> List[GraphNode]:
132
+ """
133
+ Retrieves nodes that depend on `node_id`.
134
+ (Follows incoming DEPENDS_ON, REQUIRES, IMPLEMENTS, IMPORTS edges).
135
+ """
136
+ dependency_relations = {
137
+ RelationType.DEPENDS_ON,
138
+ RelationType.REQUIRES,
139
+ RelationType.IMPLEMENTS,
140
+ RelationType.IMPORTS,
141
+ }
142
+
143
+ visited: Set[str] = set()
144
+ queue: deque = deque()
145
+
146
+ for edge in self._in_edges.get(node_id, []):
147
+ if edge.relation in dependency_relations and edge.source_id in self._nodes:
148
+ queue.append(edge.source_id)
149
+ visited.add(edge.source_id)
150
+
151
+ results: List[GraphNode] = []
152
+
153
+ while queue:
154
+ curr_id = queue.popleft()
155
+ node = self._nodes.get(curr_id)
156
+ if node:
157
+ results.append(node)
158
+
159
+ if recursive:
160
+ for edge in self._in_edges.get(curr_id, []):
161
+ if edge.relation in dependency_relations:
162
+ if edge.source_id not in visited and edge.source_id in self._nodes:
163
+ visited.add(edge.source_id)
164
+ queue.append(edge.source_id)
165
+
166
+ return results
167
+
168
+ def detect_cycles(self) -> List[List[str]]:
169
+ """
170
+ Detects all elementary directed cycles in the graph using DFS recursion.
171
+ Returns list of cycle paths (e.g. [['A', 'B', 'C', 'A']]).
172
+ """
173
+ visited: Dict[str, int] = {} # 0 = unvisited, 1 = visiting (in stack), 2 = visited
174
+ cycles: List[List[str]] = []
175
+ path: List[str] = []
176
+
177
+ for node_id in self._nodes:
178
+ visited[node_id] = 0
179
+
180
+ def dfs(u: str):
181
+ visited[u] = 1
182
+ path.append(u)
183
+
184
+ for edge in self._out_edges.get(u, []):
185
+ v = edge.target_id
186
+ if v not in visited:
187
+ continue
188
+ if visited[v] == 1:
189
+ # Cycle detected: extract subpath from v to end
190
+ cycle_start_idx = path.index(v)
191
+ cycle_path = path[cycle_start_idx:] + [v]
192
+ cycles.append(cycle_path)
193
+ elif visited[v] == 0:
194
+ dfs(v)
195
+
196
+ path.pop()
197
+ visited[u] = 2
198
+
199
+ for node_id in list(self._nodes.keys()):
200
+ if visited[node_id] == 0:
201
+ dfs(node_id)
202
+
203
+ return cycles
204
+
205
+ def validate_graph(self) -> GraphValidationResult:
206
+ """
207
+ Verifies complete topological integrity of the DAG.
208
+ """
209
+ cycles = self.detect_cycles()
210
+ missing_refs: List[str] = []
211
+ orphan_nodes: List[str] = []
212
+ errors: List[str] = []
213
+
214
+ # Check missing endpoints
215
+ for edge in self._edges:
216
+ if edge.source_id not in self._nodes:
217
+ missing_refs.append(f"Edge source '{edge.source_id}' does not exist as a node")
218
+ if edge.target_id not in self._nodes:
219
+ missing_refs.append(f"Edge target '{edge.target_id}' does not exist as a node")
220
+
221
+ # Check orphan nodes (nodes with 0 incoming and 0 outgoing edges)
222
+ for node_id in self._nodes:
223
+ has_in = bool(self._in_edges.get(node_id))
224
+ has_out = bool(self._out_edges.get(node_id))
225
+ if not has_in and not has_out:
226
+ orphan_nodes.append(node_id)
227
+
228
+ if cycles:
229
+ errors.append(f"Detected {len(cycles)} circular dependency cycle(s)")
230
+ if missing_refs:
231
+ errors.extend(missing_refs)
232
+
233
+ is_valid = (len(cycles) == 0 and len(missing_refs) == 0)
234
+
235
+ return GraphValidationResult(
236
+ is_valid=is_valid,
237
+ cycles_detected=cycles,
238
+ missing_endpoint_references=missing_refs,
239
+ orphan_node_ids=orphan_nodes,
240
+ errors=errors
241
+ )
242
+
243
+ def get_stats(self) -> GraphStats:
244
+ """Calculates macro metrics of the graph."""
245
+ nodes_by_type: Dict[str, int] = defaultdict(int)
246
+ for n in self._nodes.values():
247
+ nodes_by_type[n.node_type.value] += 1
248
+
249
+ edges_by_relation: Dict[str, int] = defaultdict(int)
250
+ for e in self._edges:
251
+ edges_by_relation[e.relation.value] += 1
252
+
253
+ roots = [nid for nid in self._nodes if not self._in_edges.get(nid)]
254
+ leaves = [nid for nid in self._nodes if not self._out_edges.get(nid)]
255
+
256
+ return GraphStats(
257
+ total_nodes=len(self._nodes),
258
+ total_edges=len(self._edges),
259
+ nodes_by_type=dict(nodes_by_type),
260
+ edges_by_relation=dict(edges_by_relation),
261
+ root_node_ids=roots,
262
+ leaf_node_ids=leaves
263
+ )
264
+
265
+ def propagate_invalidation(self, changed_node_id: str, new_status: Optional[Status] = None) -> List[str]:
266
+ """
267
+ Propagates status invalidation to all downstream dependents when a dependency changes.
268
+ Marks affected dependents STALE (or BLOCKED if failed).
269
+ Implements IStateGraphManager Protocol.
270
+ """
271
+ from graph.propagator import DependencyPropagator
272
+ propagator = DependencyPropagator(self)
273
+ res = propagator.propagate_change(changed_node_id=changed_node_id, new_status=new_status)
274
+ return res.affected_node_ids
275
+
276
+ def get_unresolved_dependencies(self, node_id: str) -> List[GraphNode]:
277
+ """Returns direct upstream prerequisites that are not VERIFIED."""
278
+ from graph.propagator import DependencyPropagator
279
+ propagator = DependencyPropagator(self)
280
+ return propagator.get_unresolved_dependencies(node_id)
281
+
282
+ def get_actionable_nodes(self) -> List[GraphNode]:
283
+ """Returns nodes whose prerequisites are satisfied and ready to work on."""
284
+ from graph.propagator import DependencyPropagator
285
+ propagator = DependencyPropagator(self)
286
+ return propagator.determine_next_actionable_nodes()
287
+
288
+ def recommend_next_action(self) -> Optional[Any]:
289
+ """Synthesizes the optimal next tactical action."""
290
+ from graph.propagator import DependencyPropagator
291
+ propagator = DependencyPropagator(self)
292
+ return propagator.recommend_next_action()
293
+
294
+ def export_mermaid(self) -> str:
295
+ """
296
+ Generates Mermaid diagram representing the DAG with status and type styling.
297
+ """
298
+ lines = ["graph TD"]
299
+
300
+ # Status color styles
301
+ style_classes = [
302
+ " classDef verified fill:#2ecc71,stroke:#27ae60,stroke-width:2px,color:#fff;",
303
+ " classDef failed fill:#e74c3c,stroke:#c0392b,stroke-width:2px,color:#fff;",
304
+ " classDef in_progress fill:#f39c12,stroke:#d35400,stroke-width:2px,color:#fff;",
305
+ " classDef unverified fill:#95a5a6,stroke:#7f8c8d,stroke-width:2px,color:#fff;",
306
+ " classDef stale fill:#e67e22,stroke:#d35400,stroke-width:2px,color:#fff;",
307
+ " classDef unknown fill:#bdc3c7,stroke:#95a5a6,stroke-width:1px,color:#000;"
308
+ ]
309
+
310
+ # 1. Add Nodes
311
+ for node in sorted(self._nodes.values(), key=lambda n: n.id):
312
+ clean_id = node.id.replace("-", "_").replace(".", "_")
313
+ label = f"{node.name} [{node.node_type.value}]<br/>({node.status.value}, {node.confidence_score:.0f}%)"
314
+ lines.append(f' {clean_id}["{label}"]')
315
+
316
+ # 2. Add Edges
317
+ relation_labels = {
318
+ RelationType.DEPENDS_ON: "depends_on",
319
+ RelationType.IMPLEMENTS: "implements",
320
+ RelationType.VERIFIES: "verifies",
321
+ RelationType.BLOCKS: "blocks",
322
+ RelationType.IMPORTS: "imports",
323
+ RelationType.REQUIRES: "requires",
324
+ RelationType.INVALIDATES: "invalidates",
325
+ }
326
+
327
+ for edge in sorted(self._edges, key=lambda e: (e.source_id, e.target_id)):
328
+ src = edge.source_id.replace("-", "_").replace(".", "_")
329
+ tgt = edge.target_id.replace("-", "_").replace(".", "_")
330
+ rel_label = relation_labels.get(edge.relation, edge.relation.value)
331
+ lines.append(f' {src} -->|"{rel_label}"| {tgt}')
332
+
333
+ # 3. Add styling classes to nodes
334
+ status_map = {
335
+ Status.VERIFIED: "verified",
336
+ Status.FAILED: "failed",
337
+ Status.IN_PROGRESS: "in_progress",
338
+ Status.UNVERIFIED: "unverified",
339
+ Status.STALE: "stale",
340
+ Status.UNKNOWN: "unknown"
341
+ }
342
+
343
+ for node in self._nodes.values():
344
+ clean_id = node.id.replace("-", "_").replace(".", "_")
345
+ cls_name = status_map.get(node.status, "unknown")
346
+ lines.append(f" class {clean_id} {cls_name};")
347
+
348
+ lines.extend(style_classes)
349
+ return "\n".join(lines)
350
+
351
+ def build_from_canonical_state(self, canonical_state: CanonicalProjectState) -> None:
352
+ """
353
+ Constructs the DAG automatically from CanonicalProjectState entities:
354
+ - Creates Requirement nodes from conversational state.
355
+ - Creates Module / Symbol nodes from project state AST.
356
+ - Creates Test nodes from test results.
357
+ - Links Tests $\\to$ Symbols via VERIFIES.
358
+ - Links Symbols $\\to$ Requirements via IMPLEMENTS.
359
+ """
360
+ self._canonical_state = canonical_state
361
+
362
+ # 1. Add Requirement Nodes
363
+ for req in canonical_state.conversational_state.user_requirements:
364
+ ev_ids = [req.evidence_id] if req.evidence_id else []
365
+ self.add_node(GraphNode(
366
+ id=f"req_{req.id}",
367
+ name=req.title,
368
+ node_type=NodeType.REQUIREMENT,
369
+ status=req.status,
370
+ evidence_ids=ev_ids,
371
+ metadata={"description": req.description}
372
+ ))
373
+
374
+ # 2. Add AST Symbol / Component Nodes
375
+ for sym in canonical_state.project_state.symbols:
376
+ ev_ids = [sym.evidence_id] if sym.evidence_id else []
377
+ sym_node_id = f"sym_{sym.name}"
378
+ self.add_node(GraphNode(
379
+ id=sym_node_id,
380
+ name=sym.name,
381
+ node_type=NodeType.COMPONENT if sym.kind in {"class", "interface"} else NodeType.API,
382
+ status=Status.VERIFIED,
383
+ evidence_ids=ev_ids,
384
+ metadata={"file_path": sym.file_path, "kind": sym.kind}
385
+ ))
386
+
387
+ # Attempt link to requirements with matching names
388
+ for req in canonical_state.conversational_state.user_requirements:
389
+ if req.title.lower() in sym.name.lower() or sym.name.lower() in req.title.lower():
390
+ self.add_edge(GraphEdge(
391
+ source_id=sym_node_id,
392
+ target_id=f"req_{req.id}",
393
+ relation=RelationType.IMPLEMENTS
394
+ ))
395
+
396
+ # 3. Add Test Nodes and link to verified components
397
+ for test in canonical_state.project_state.test_results:
398
+ ev_ids = [test.evidence_id] if test.evidence_id else []
399
+ test_node_id = f"test_{test.test_id}"
400
+ self.add_node(GraphNode(
401
+ id=test_node_id,
402
+ name=test.name,
403
+ node_type=NodeType.TEST,
404
+ status=test.status,
405
+ evidence_ids=ev_ids,
406
+ metadata={"suite": test.suite, "exit_code": test.exit_code}
407
+ ))
408
+
409
+ # Find matching component being tested
410
+ for sym in canonical_state.project_state.symbols:
411
+ sym_clean = sym.name.lower().replace("service", "").replace("controller", "").replace("manager", "").replace("client", "")
412
+ if (
413
+ sym.name.lower() in test.name.lower()
414
+ or sym.name.lower() in test.suite.lower()
415
+ or (sym_clean and sym_clean in test.name.lower())
416
+ or (sym_clean and sym_clean in test.suite.lower())
417
+ ):
418
+ self.add_edge(GraphEdge(
419
+ source_id=test_node_id,
420
+ target_id=f"sym_{sym.name}",
421
+ relation=RelationType.VERIFIES
422
+ ))
423
+
424
+ def save_snapshot(self, file_path: str, metadata: Optional[Dict[str, Any]] = None) -> str:
425
+ """Saves current graph state to a versioned atomic snapshot file."""
426
+ from graph.snapshot import GraphSnapshot
427
+ snapshot = GraphSnapshot.from_graph_manager(self, metadata=metadata)
428
+ snapshot.save(file_path)
429
+ return snapshot.snapshot_id
430
+
431
+ def load_snapshot(self, file_path: str) -> None:
432
+ """Loads and restores graph state from a snapshot file."""
433
+ from graph.snapshot import GraphSnapshot
434
+ snapshot = GraphSnapshot.load(file_path)
435
+ self._nodes.clear()
436
+ self._edges.clear()
437
+ self._out_edges.clear()
438
+ self._in_edges.clear()
439
+
440
+ for nd in snapshot.nodes:
441
+ self.add_node(GraphNode.from_dict(nd))
442
+ for ed in snapshot.edges:
443
+ self.add_edge(GraphEdge.from_dict(ed))
444
+
445
+ def compare_with(self, other_manager: "StateGraphManager") -> Any:
446
+ """Compares current graph against another manager instance."""
447
+ from graph.snapshot import GraphSnapshot
448
+ from graph.diff import GraphDiff
449
+ snap_a = GraphSnapshot.from_graph_manager(self)
450
+ snap_b = GraphSnapshot.from_graph_manager(other_manager)
451
+ return GraphDiff.compare(snap_a, snap_b)
452
+
453
+ def query(self) -> Any:
454
+ """Returns GraphQueryEngine instance for active queries."""
455
+ from graph.query import GraphQueryEngine
456
+ return GraphQueryEngine(self)
457
+
458
+ def export_dot(self) -> str:
459
+ """Generates Graphviz DOT representation of the DAG."""
460
+ lines = ["digraph CanonicalStateGraph {", " rankdir=LR;", " node [shape=box, style=rounded, fontname=\"Helvetica\"];"]
461
+ for node in sorted(self._nodes.values(), key=lambda n: n.id):
462
+ clean_id = f'"{node.id}"'
463
+ label = f"{node.name}\\n[{node.node_type.value}]\\n{node.status.value}"
464
+ color = "#2ecc71" if node.status == Status.VERIFIED else ("#e74c3c" if node.status == Status.FAILED else "#95a5a6")
465
+ lines.append(f" {clean_id} [label=\"{label}\", color=\"{color}\"];")
466
+
467
+ for edge in sorted(self._edges, key=lambda e: (e.source_id, e.target_id)):
468
+ src = f'"{edge.source_id}"'
469
+ tgt = f'"{edge.target_id}"'
470
+ lines.append(f" {src} -> {tgt} [label=\"{edge.relation.value}\"];")
471
+
472
+ lines.append("}")
473
+ return "\n".join(lines)
graph/models.py ADDED
@@ -0,0 +1,62 @@
1
+ """
2
+ Project Continuum - State Graph Models & Validation
3
+ ===================================================
4
+ Milestone 4 - Phase 9 & 10: State Graph (DAG) & Dependency Invalidation.
5
+ Defines graph statistics, topology validation results, traversal types, and propagation outcomes.
6
+ """
7
+
8
+ from dataclasses import dataclass, field, asdict
9
+ from typing import Any, Dict, List, Optional
10
+
11
+
12
+ @dataclass
13
+ class GraphValidationResult:
14
+ """Outcome of DAG structure and integrity verification."""
15
+ is_valid: bool
16
+ cycles_detected: List[List[str]] = field(default_factory=list)
17
+ missing_endpoint_references: List[str] = field(default_factory=list)
18
+ orphan_node_ids: List[str] = field(default_factory=list)
19
+ errors: List[str] = field(default_factory=list)
20
+
21
+ def to_dict(self) -> Dict[str, Any]:
22
+ return asdict(self)
23
+
24
+ @classmethod
25
+ def from_dict(cls, data: Dict[str, Any]) -> "GraphValidationResult":
26
+ return cls(**data)
27
+
28
+
29
+ @dataclass
30
+ class GraphStats:
31
+ """Summary metrics of the project's Canonical State Graph."""
32
+ total_nodes: int = 0
33
+ total_edges: int = 0
34
+ nodes_by_type: Dict[str, int] = field(default_factory=dict)
35
+ edges_by_relation: Dict[str, int] = field(default_factory=dict)
36
+ root_node_ids: List[str] = field(default_factory=list)
37
+ leaf_node_ids: List[str] = field(default_factory=list)
38
+
39
+ def to_dict(self) -> Dict[str, Any]:
40
+ return asdict(self)
41
+
42
+ @classmethod
43
+ def from_dict(cls, data: Dict[str, Any]) -> "GraphStats":
44
+ return cls(**data)
45
+
46
+
47
+ @dataclass
48
+ class PropagationResult:
49
+ """Outcome of dependency status propagation and invalidation."""
50
+ changed_node_id: str
51
+ affected_node_ids: List[str] = field(default_factory=list)
52
+ stale_node_ids: List[str] = field(default_factory=list)
53
+ blocked_node_ids: List[str] = field(default_factory=list)
54
+ unresolved_prerequisites: Dict[str, List[str]] = field(default_factory=dict)
55
+ explanation: str = ""
56
+
57
+ def to_dict(self) -> Dict[str, Any]:
58
+ return asdict(self)
59
+
60
+ @classmethod
61
+ def from_dict(cls, data: Dict[str, Any]) -> "PropagationResult":
62
+ return cls(**data)