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.
- cli/__init__.py +12 -0
- cli/main.py +415 -0
- confidence/__init__.py +13 -0
- confidence/calculator.py +308 -0
- confidence/models.py +40 -0
- context/__init__.py +17 -0
- context/models.py +108 -0
- context/pruner.py +228 -0
- context/selector.py +193 -0
- continuum_toolkit-1.0.0.dist-info/METADATA +511 -0
- continuum_toolkit-1.0.0.dist-info/RECORD +72 -0
- continuum_toolkit-1.0.0.dist-info/WHEEL +5 -0
- continuum_toolkit-1.0.0.dist-info/entry_points.txt +2 -0
- continuum_toolkit-1.0.0.dist-info/licenses/LICENSE +21 -0
- continuum_toolkit-1.0.0.dist-info/top_level.txt +13 -0
- contradictions/__init__.py +19 -0
- contradictions/detector.py +442 -0
- contradictions/models.py +75 -0
- core/__init__.py +97 -0
- core/enums.py +130 -0
- core/evidence.py +117 -0
- core/interfaces.py +209 -0
- core/schema.py +391 -0
- core/serializer.py +84 -0
- core/state_models.py +530 -0
- daemon/__init__.py +12 -0
- daemon/service.py +170 -0
- extractors/__init__.py +51 -0
- extractors/base.py +117 -0
- extractors/config/parsers.py +288 -0
- extractors/config/secret_sanitizer.py +91 -0
- extractors/config_extractor.py +172 -0
- extractors/conversation/analyzers.py +193 -0
- extractors/conversation/models.py +148 -0
- extractors/conversation_extractor.py +166 -0
- extractors/git_extractor.py +305 -0
- extractors/parsers/base.py +91 -0
- extractors/parsers/comment_parser.py +51 -0
- extractors/parsers/js_ts_parser.py +171 -0
- extractors/parsers/python_parser.py +180 -0
- extractors/verification/runners.py +278 -0
- extractors/verification_extractor.py +263 -0
- extractors/workspace_extractor.py +221 -0
- graph/__init__.py +24 -0
- graph/diff.py +109 -0
- graph/manager.py +473 -0
- graph/models.py +62 -0
- graph/propagator.py +194 -0
- graph/query.py +86 -0
- graph/snapshot.py +85 -0
- handoff/__init__.py +28 -0
- handoff/adapters/__init__.py +45 -0
- handoff/adapters/base.py +90 -0
- handoff/adapters/claude_adapter.py +176 -0
- handoff/adapters/codex_gpt_adapter.py +151 -0
- handoff/adapters/gemini_adapter.py +151 -0
- handoff/adapters/local_model_adapter.py +130 -0
- handoff/models.py +88 -0
- handoff/packager.py +288 -0
- pipeline/__init__.py +9 -0
- pipeline/orchestrator.py +260 -0
- resolution/__init__.py +15 -0
- resolution/resolver.py +311 -0
- storage/__init__.py +18 -0
- storage/hooks.py +125 -0
- storage/manager.py +127 -0
- storage/models.py +39 -0
- storage/recovery.py +98 -0
- watcher/__init__.py +17 -0
- watcher/detector.py +136 -0
- watcher/models.py +62 -0
- 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)
|