polymath-nodeos 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.
@@ -0,0 +1,246 @@
1
+ import math
2
+ import sqlite3
3
+ import sys
4
+
5
+ class Point:
6
+ def __init__(self, x, y, node_id, node_type, mass=1.0, name="unknown", calls=None, filepath=None):
7
+ self.x = x
8
+ self.y = y
9
+ self.vx = 0.0
10
+ self.vy = 0.0
11
+ self.fx = 0.0
12
+ self.fy = 0.0
13
+ self.mass = mass
14
+ self.node_id = node_id
15
+ self.node_type = node_type
16
+ self.name = name
17
+ self.calls = calls if calls else []
18
+ self.edges = []
19
+ self.filepath = filepath
20
+
21
+ class Rectangle:
22
+ def __init__(self, x, y, w, h):
23
+ self.x = x
24
+ self.y = y
25
+ self.w = w
26
+ self.h = h
27
+
28
+ def contains(self, point):
29
+ return (self.x - self.w <= point.x <= self.x + self.w and
30
+ self.y - self.h <= point.y <= self.y + self.h)
31
+
32
+ def intersects(self, range_rect):
33
+ return not (range_rect.x - range_rect.w > self.x + self.w or
34
+ range_rect.x + range_rect.w < self.x - self.w or
35
+ range_rect.y - range_rect.h > self.y + self.h or
36
+ range_rect.y + range_rect.h < self.y - self.h)
37
+
38
+ class QuadTree:
39
+ def __init__(self, boundary, capacity):
40
+ self.boundary = boundary
41
+ self.capacity = capacity
42
+ self.points = []
43
+ self.divided = False
44
+
45
+ def subdivide(self):
46
+ x, y, w, h = self.boundary.x, self.boundary.y, self.boundary.w / 2, self.boundary.h / 2
47
+ self.northeast = QuadTree(Rectangle(x + w, y - h, w, h), self.capacity)
48
+ self.northwest = QuadTree(Rectangle(x - w, y - h, w, h), self.capacity)
49
+ self.southeast = QuadTree(Rectangle(x + w, y + h, w, h), self.capacity)
50
+ self.southwest = QuadTree(Rectangle(x - w, y + h, w, h), self.capacity)
51
+ self.divided = True
52
+
53
+ def insert(self, point):
54
+ if not self.boundary.contains(point):
55
+ return False
56
+ if len(self.points) < self.capacity:
57
+ self.points.append(point)
58
+ return True
59
+ if not self.divided:
60
+ self.subdivide()
61
+ return (self.northeast.insert(point) or self.northwest.insert(point) or
62
+ self.southeast.insert(point) or self.southwest.insert(point))
63
+
64
+ def query(self, range_rect, found=None):
65
+ if found is None: found = []
66
+ if not self.boundary.intersects(range_rect): return found
67
+ for p in self.points:
68
+ if range_rect.contains(p): found.append(p)
69
+ if self.divided:
70
+ self.northwest.query(range_rect, found)
71
+ self.northeast.query(range_rect, found)
72
+ self.southwest.query(range_rect, found)
73
+ self.southeast.query(range_rect, found)
74
+ return found
75
+
76
+ class NativeNodesEngine:
77
+ """
78
+ NATIVE SPATIAL CLUSTERING & PHYSICS ENGINE
79
+ Implements Force-Directed Graph physics (Repulsion + Gravity + Hooke's Law Springs) and batch SQLite sync.
80
+ """
81
+ def __init__(self, db_path='agy_nodeos.db'):
82
+ self.db_path = db_path
83
+ self.boundary = Rectangle(500, 500, 500, 500)
84
+ self.qtree = QuadTree(self.boundary, 4)
85
+ self.all_nodes = []
86
+ self.hydrate_from_db()
87
+
88
+ def hydrate_from_db(self):
89
+ print("[Physics Engine] Hydrating spatial matrix from SQLite Database...")
90
+ conn = sqlite3.connect(self.db_path)
91
+ cursor = conn.cursor()
92
+ try:
93
+ cursor.execute("SELECT node_id, node_type, x_coord, y_coord, name, filepath FROM nodes")
94
+ rows = cursor.fetchall()
95
+ for row in rows:
96
+ if row[2] is not None and row[3] is not None:
97
+ name = row[4] if len(row) > 4 else "unknown"
98
+ filepath = row[5] if len(row) > 5 else None
99
+ p = Point(row[2], row[3], row[0], row[1], name=name, filepath=filepath)
100
+ self.all_nodes.append(p)
101
+ self.qtree.insert(p)
102
+ print(f"[Physics Engine] Hydrated {len(self.all_nodes)} kinetic nodes.")
103
+ except sqlite3.OperationalError:
104
+ print("[Physics Engine] Table not initialized. Skipping hydration.")
105
+ conn.close()
106
+
107
+ def rebuild_qtree(self):
108
+ self.qtree = QuadTree(self.boundary, 4)
109
+ for p in self.all_nodes:
110
+ self.qtree.insert(p)
111
+
112
+ def resolve_edges(self):
113
+ """Resolves the string AST calls into physical Point references for Spring Physics."""
114
+ print("[Physics Engine] Resolving execution call graph into physical edges...")
115
+ name_to_node = {p.name: p for p in self.all_nodes}
116
+ edge_count = 0
117
+
118
+ conn = sqlite3.connect(self.db_path)
119
+ cursor = conn.cursor()
120
+
121
+ for p in self.all_nodes:
122
+ for call_name in p.calls:
123
+ target = name_to_node.get(call_name)
124
+ if target and target not in p.edges:
125
+ p.edges.append(target)
126
+ edge_count += 1
127
+ try:
128
+ cursor.execute('''
129
+ INSERT OR IGNORE INTO edges (source_id, target_id, relation_type)
130
+ VALUES (?, ?, "CALL")
131
+ ''', (p.node_id, target.node_id))
132
+ except sqlite3.OperationalError:
133
+ pass # Ignore if edges table missing/already exists
134
+
135
+ conn.commit()
136
+ conn.close()
137
+ print(f"[Physics Engine] Bonded {edge_count} structural connections. Simulating graph clustering...")
138
+ if edge_count > 0:
139
+ import threading
140
+ # Run physics simulation in the background so it doesn't block the Daemon event loop
141
+ threading.Thread(target=self.simulate_physics, args=(100,), daemon=True).start()
142
+
143
+ def simulate_physics(self, ticks=50):
144
+ """Euler Integration of Coulomb Repulsion, Central Gravity, and Hooke's Law Springs."""
145
+ damping = 0.85
146
+ time_step = 1.0
147
+ k_repulse = 5000.0
148
+ k_gravity = 0.05
149
+ k_spring = 0.1 # Hooke's Law spring constant
150
+ ideal_length = 50.0 # Ideal distance between connected nodes
151
+ center_x, center_y = 500.0, 500.0
152
+
153
+ for tick in range(ticks):
154
+ # Rebuild QuadTree for rapid O(N log N) spatial queries
155
+ self.rebuild_qtree()
156
+
157
+ for node in self.all_nodes:
158
+ node.fx = (center_x - node.x) * k_gravity
159
+ node.fy = (center_y - node.y) * k_gravity
160
+
161
+ # Optimized Local Repulsion: Only repel against nodes within 150px radius using QuadTree
162
+ range_rect = Rectangle(node.x, node.y, 150, 150)
163
+ nearby_nodes = self.qtree.query(range_rect)
164
+
165
+ for other in nearby_nodes:
166
+ if other == node:
167
+ continue
168
+ dx, dy = node.x - other.x, node.y - other.y
169
+ dist_sq = dx**2 + dy**2
170
+ if dist_sq > 0:
171
+ force = k_repulse / dist_sq
172
+ dist = math.sqrt(dist_sq)
173
+ node.fx += force * (dx / dist)
174
+ node.fy += force * (dy / dist)
175
+
176
+ # Hooke's Law Spring Attraction between CONNECTED nodes
177
+ for n1 in self.all_nodes:
178
+ for n2 in n1.edges:
179
+ dx, dy = n2.x - n1.x, n2.y - n1.y
180
+ dist = math.sqrt(dx**2 + dy**2)
181
+ if dist > 0:
182
+ force = k_spring * (dist - ideal_length)
183
+ fx = force * (dx / dist)
184
+ fy = force * (dy / dist)
185
+ n1.fx += fx
186
+ n1.fy += fy
187
+ n2.fx -= fx
188
+ n2.fy -= fy
189
+
190
+ # Euler Integration
191
+ for node in self.all_nodes:
192
+ ax = node.fx / node.mass
193
+ ay = node.fy / node.mass
194
+ node.vx = (node.vx + ax * time_step) * damping
195
+ node.vy = (node.vy + ay * time_step) * damping
196
+ node.x += node.vx * time_step
197
+ node.y += node.vy * time_step
198
+
199
+ self.rebuild_qtree()
200
+ self.sync_to_sqlite()
201
+ sys.stderr.write(f"[Physics Engine] Successfully settled {len(self.all_nodes)} nodes. Syncing to SQLite.\n")
202
+
203
+ def sync_to_sqlite(self):
204
+ """Batch update physical positions to SQLite to save I/O overhead."""
205
+ conn = sqlite3.connect(self.db_path)
206
+ cursor = conn.cursor()
207
+ for p in self.all_nodes:
208
+ cursor.execute('''
209
+ INSERT INTO nodes (node_id, node_type, name, filepath, x_coord, y_coord, last_updated)
210
+ VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
211
+ ON CONFLICT(node_id) DO UPDATE SET
212
+ name=excluded.name,
213
+ filepath=excluded.filepath,
214
+ x_coord=excluded.x_coord,
215
+ y_coord=excluded.y_coord,
216
+ last_updated=excluded.last_updated
217
+ ''', (p.node_id, p.node_type, p.name, p.filepath, p.x, p.y))
218
+ conn.commit()
219
+ conn.close()
220
+
221
+ def remove_nodes_by_file(self, filepath):
222
+ """Removes all spatial nodes belonging to a deleted file."""
223
+ sys.stderr.write(f"[Physics Engine] Removing spatial nodes for deleted file: {filepath}\n")
224
+ self.all_nodes = [node for node in self.all_nodes if node.filepath != filepath]
225
+ for node in self.all_nodes:
226
+ node.edges = [e for e in node.edges if e.filepath != filepath]
227
+ self.rebuild_qtree()
228
+
229
+ def add_node(self, node_id, node_type, name="unknown", calls=None, parent_x=500, parent_y=500, filepath=None):
230
+ import random
231
+ drop_x = parent_x + random.uniform(-10, 10)
232
+ drop_y = parent_y + random.uniform(-10, 10)
233
+
234
+ existing = next((n for n in self.all_nodes if n.node_id == node_id), None)
235
+ if not existing:
236
+ p = Point(drop_x, drop_y, node_id, node_type, name=name, calls=calls, filepath=filepath)
237
+ self.all_nodes.append(p)
238
+ sys.stderr.write(f"[Physics Engine] Dropped {node_type} '{name}' into kinetic simulation.\n")
239
+ return p
240
+ else:
241
+ existing.name = name
242
+ if filepath:
243
+ existing.filepath = filepath
244
+ if calls:
245
+ existing.calls = calls
246
+ return existing
File without changes
@@ -0,0 +1,20 @@
1
+ import sys
2
+ import json
3
+ import telemetry
4
+
5
+ def invoke(payload_str):
6
+ try:
7
+ workflow = json.loads(payload_str)
8
+ with open('workflow.json', 'w') as f:
9
+ json.dump(workflow, f, indent=4)
10
+
11
+ # Now natively stream the NodeOS telemetry to AGY's sys.stdout
12
+ telemetry.track_workflow('workflow.json')
13
+ except Exception as e:
14
+ print(f"Failed to invoke workflow: {e}")
15
+
16
+ if __name__ == "__main__":
17
+ if len(sys.argv) > 1:
18
+ invoke(sys.argv[1])
19
+ else:
20
+ print("Usage: python agy_invoke.py '<json_string>'")
@@ -0,0 +1,127 @@
1
+ import sqlite3
2
+ import json
3
+ import os
4
+ import logging
5
+ from typing import Optional
6
+
7
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - [%(levelname)s] - %(message)s')
8
+ logger = logging.getLogger(__name__)
9
+
10
+ class BlastRadiusEngine:
11
+ """
12
+ Engine to calculate the blast radius of file modifications using a Recursive CTE
13
+ against the NodeOS SQLite graph.
14
+ """
15
+
16
+ def __init__(self, db_path: str, workspace: str, threshold: int = 10):
17
+ self.db_path = db_path
18
+ self.workspace = workspace
19
+ self.threshold = threshold
20
+
21
+ def get_blast_radius(self, filepath: str) -> int:
22
+ """
23
+ Calculates the number of downstream nodes that depend on the given filepath.
24
+
25
+ Args:
26
+ filepath: The path of the modified file.
27
+
28
+ Returns:
29
+ int: The number of distinct downstream nodes impacted.
30
+ """
31
+ if not os.path.exists(self.db_path):
32
+ logger.error(f"Database not found at {self.db_path}")
33
+ return 0
34
+
35
+ try:
36
+ with sqlite3.connect(self.db_path) as conn:
37
+ cursor = conn.cursor()
38
+
39
+ query = '''
40
+ WITH RECURSIVE downstream AS (
41
+ -- Base Case: Nodes that directly call nodes in the modified file
42
+ SELECT e.source_id
43
+ FROM edges e
44
+ JOIN nodes n ON e.target_id = n.node_id
45
+ WHERE n.filepath = ?
46
+
47
+ UNION
48
+
49
+ -- Recursive Step: Nodes that call the nodes found in the previous step
50
+ SELECT e.source_id
51
+ FROM edges e
52
+ INNER JOIN downstream d ON e.target_id = d.source_id
53
+ )
54
+ SELECT COUNT(DISTINCT source_id) FROM downstream;
55
+ '''
56
+ cursor.execute(query, (filepath,))
57
+ result = cursor.fetchone()
58
+ return result[0] if result else 0
59
+ except sqlite3.Error as e:
60
+ logger.error(f"SQLite error while calculating blast radius: {e}")
61
+ return 0
62
+
63
+ def enforce_threshold(self, filepath: str) -> None:
64
+ """
65
+ Checks the blast radius for the filepath and emits warnings if it exceeds the threshold.
66
+
67
+ Args:
68
+ filepath: The path of the modified file.
69
+ """
70
+ radius_count = self.get_blast_radius(filepath)
71
+ logger.info(f"Blast radius for {filepath}: {radius_count} downstream nodes.")
72
+
73
+ if radius_count > self.threshold:
74
+ logger.warning(f"High Blast Radius ({radius_count} dependents) for {filepath}")
75
+ self._drop_intent(filepath, radius_count)
76
+ self._write_warning_file(filepath, radius_count)
77
+
78
+ def _drop_intent(self, filepath: str, radius_count: int) -> None:
79
+ """
80
+ Drops a workflow.json intent to notify the agent swarm.
81
+ """
82
+ workflow_file = os.path.join(self.workspace, "workflow.json")
83
+ workflow = {
84
+ "type": "JSON_Task",
85
+ "status": "pending",
86
+ "action": "blast_radius_warning",
87
+ "target_file": filepath,
88
+ "impacted_nodes_count": radius_count
89
+ }
90
+
91
+ try:
92
+ with open(workflow_file, 'w') as f:
93
+ json.dump(workflow, f, indent=4)
94
+ logger.info(f"Dropped intent at {workflow_file}")
95
+ except IOError as e:
96
+ logger.error(f"Failed to write workflow.json: {e}")
97
+
98
+ def _write_warning_file(self, filepath: str, radius_count: int) -> None:
99
+ """
100
+ Writes a warning.md file in the same directory as the modified file.
101
+ """
102
+ warning_path = os.path.join(os.path.dirname(filepath), "warning.md")
103
+
104
+ content = (
105
+ f"# High Blast Radius Warning\n"
106
+ f"Modifying `{filepath}` impacts {radius_count} downstream nodes. "
107
+ f"Proceed with caution.\n"
108
+ )
109
+
110
+ try:
111
+ with open(warning_path, 'w') as w_file:
112
+ w_file.write(content)
113
+ logger.info(f"Wrote warning markdown at {warning_path}")
114
+ except IOError as e:
115
+ logger.error(f"Failed to write warning.md: {e}")
116
+
117
+ if __name__ == "__main__":
118
+ import argparse
119
+ parser = argparse.ArgumentParser(description="Calculate Blast Radius for a file in AGY-NodeOS.")
120
+ parser.add_argument("--db", required=True, help="Path to agy_nodeos.db")
121
+ parser.add_argument("--workspace", required=True, help="Path to workspace root")
122
+ parser.add_argument("--file", required=True, help="Modified filepath to check")
123
+ parser.add_argument("--threshold", type=int, default=10, help="Blast radius threshold")
124
+
125
+ args = parser.parse_args()
126
+ engine = BlastRadiusEngine(db_path=args.db, workspace=args.workspace, threshold=args.threshold)
127
+ engine.enforce_threshold(args.file)
@@ -0,0 +1,103 @@
1
+ import sqlite3
2
+ import json
3
+ import os
4
+ import time
5
+ from typing import List, Dict, Any
6
+ from dataclasses import dataclass
7
+
8
+ @dataclass
9
+ class OrphanedNode:
10
+ node_id: str
11
+ filepath: str
12
+ node_type: str
13
+
14
+ class SemanticDeadCodeEngine:
15
+ """
16
+ Advanced engine for eradicating semantic dead code in AGY-NodeOS.
17
+ Queries the SQLite dependency graph to identify orphaned nodes
18
+ and generates swarm intents for safe deletion.
19
+ """
20
+
21
+ def __init__(self, db_path: str, workspace_root: str):
22
+ self.db_path = db_path
23
+ self.workspace_root = workspace_root
24
+ self.workflow_path = os.path.join(self.workspace_root, "workflow.json")
25
+
26
+ def check_database_exists(self) -> bool:
27
+ """Verifies if the agy_nodeos.db exists."""
28
+ return os.path.exists(self.db_path)
29
+
30
+ def find_orphaned_nodes(self) -> List[OrphanedNode]:
31
+ """
32
+ Executes the SQL query to find nodes with 0 incoming edges.
33
+ Excludes 'entry_point' nodes to prevent removing roots.
34
+ """
35
+ if not self.check_database_exists():
36
+ raise FileNotFoundError(f"Database not found at {self.db_path}")
37
+
38
+ with sqlite3.connect(self.db_path) as conn:
39
+ cursor = conn.cursor()
40
+ query = """
41
+ SELECT n.node_id, n.filepath, n.node_type
42
+ FROM nodes n
43
+ LEFT JOIN edges e ON n.node_id = e.target_id
44
+ WHERE e.source_id IS NULL
45
+ AND n.node_type != 'entry_point';
46
+ """
47
+ cursor.execute(query)
48
+ rows = cursor.fetchall()
49
+
50
+ return [OrphanedNode(node_id=r[0], filepath=r[1], node_type=r[2]) for r in rows]
51
+
52
+ def _build_intent_payload(self, nodes: List[OrphanedNode]) -> Dict[str, Any]:
53
+ """Constructs the intent JSON payload based on NodeOS schema."""
54
+ targets = [
55
+ {
56
+ "id": node.node_id,
57
+ "file_path": node.filepath,
58
+ "action": "delete"
59
+ }
60
+ for node in nodes
61
+ ]
62
+
63
+ return {
64
+ "intent": "eradicate_dead_code",
65
+ "status": "Pending",
66
+ "targets": targets,
67
+ "context": "Node has 0 incoming edges in agy_nodeos.db",
68
+ "timestamp": time.time()
69
+ }
70
+
71
+ def generate_deletion_intents(self) -> None:
72
+ """
73
+ Identifies orphaned nodes and dispatches a deletion intent to workflow.json.
74
+ """
75
+ orphans = self.find_orphaned_nodes()
76
+ if not orphans:
77
+ print("[DeadCodeEngine] No orphaned nodes detected. System clean.")
78
+ return
79
+
80
+ print(f"[DeadCodeEngine] Discovered {len(orphans)} orphaned nodes. Generating intent...")
81
+ intent_payload = self._build_intent_payload(orphans)
82
+
83
+ # Write intent to workflow.json to trigger swarm intelligence
84
+ with open(self.workflow_path, "w") as f:
85
+ json.dump(intent_payload, f, indent=2)
86
+
87
+ print(f"[DeadCodeEngine] Intent dispatched to {self.workflow_path}")
88
+
89
+
90
+ def main():
91
+ # Resolve paths relative to this script location
92
+ script_dir = os.path.dirname(os.path.abspath(__file__))
93
+ workspace_root = os.path.abspath(os.path.join(script_dir, ".."))
94
+ db_path = os.path.join(workspace_root, "agy_nodeos.db")
95
+
96
+ engine = SemanticDeadCodeEngine(db_path=db_path, workspace_root=workspace_root)
97
+ try:
98
+ engine.generate_deletion_intents()
99
+ except Exception as e:
100
+ print(f"[DeadCodeEngine] Error: {e}")
101
+
102
+ if __name__ == "__main__":
103
+ main()
@@ -0,0 +1,93 @@
1
+ import sqlite3
2
+ import json
3
+ import os
4
+ import argparse
5
+ from typing import List
6
+
7
+ class DominoEngine:
8
+ """
9
+ Advanced architecture module for executing the Domino Refactor blueprint.
10
+ Manages querying the AST call graph and dispatching parallel refactor swarms.
11
+ """
12
+
13
+ def __init__(self, db_path: str = "agy_nodeos.db", workflow_path: str = "workflow.json"):
14
+ self.db_path = db_path
15
+ self.workflow_path = workflow_path
16
+
17
+ def find_callers(self, target_hash: str) -> List[str]:
18
+ """
19
+ Finds all files calling a specific hash by querying the AST edges.
20
+ Uses INNER JOIN logic to resolve source and target node filepaths.
21
+ """
22
+ if not os.path.exists(self.db_path):
23
+ raise FileNotFoundError(f"Database not found at path: {self.db_path}")
24
+
25
+ query = """
26
+ SELECT DISTINCT caller.filepath
27
+ FROM edges e
28
+ JOIN nodes target ON e.target_id = target.node_id
29
+ JOIN nodes caller ON e.source_id = caller.node_id
30
+ WHERE target.hash = ? AND e.relation_type = 'CALL';
31
+ """
32
+
33
+ try:
34
+ with sqlite3.connect(self.db_path) as conn:
35
+ cursor = conn.cursor()
36
+ cursor.execute(query, (target_hash,))
37
+ rows = cursor.fetchall()
38
+ return [row[0] for row in rows]
39
+ except sqlite3.Error as e:
40
+ print(f"Database error during find_callers: {e}")
41
+ return []
42
+
43
+ def generate_intent(self, target_hash: str, callers: List[str]) -> None:
44
+ """
45
+ Generates a multi-agent parallel intent payload and writes it to workflow.json.
46
+ """
47
+ if not callers:
48
+ print(f"No callers found for hash '{target_hash}'. No workflow generated.")
49
+ return
50
+
51
+ workflow = {
52
+ "status": "pending",
53
+ "agents": []
54
+ }
55
+
56
+ for caller_file in callers:
57
+ task = {
58
+ "task": "refactor_domino",
59
+ "target_file": caller_file,
60
+ "instruction": f"Refactor function calls invoking hash {target_hash} to align with the newly updated signature."
61
+ }
62
+ workflow["agents"].append(task)
63
+
64
+ try:
65
+ with open(self.workflow_path, 'w', encoding='utf-8') as f:
66
+ json.dump(workflow, f, indent=2)
67
+ print(f"Successfully dispatched refactor swarm intent for {len(callers)} target files to {self.workflow_path}.")
68
+ except IOError as e:
69
+ print(f"IOError writing to {self.workflow_path}: {e}")
70
+
71
+ def run_refactor(self, target_hash: str) -> None:
72
+ """
73
+ Executes the end-to-end domino refactor chain for a specific hash.
74
+ """
75
+ print(f"Starting Domino Refactor for hash: {target_hash}")
76
+ callers = self.find_callers(target_hash)
77
+ self.generate_intent(target_hash, callers)
78
+
79
+
80
+ def main():
81
+ parser = argparse.ArgumentParser(description="Domino Refactor Engine")
82
+ parser.add_argument("hash", help="The specific function hash to refactor")
83
+ parser.add_argument("--db", default="agy_nodeos.db", help="Path to agy_nodeos.db SQLite database")
84
+ parser.add_argument("--workflow", default="workflow.json", help="Path to output workflow.json payload")
85
+
86
+ args = parser.parse_args()
87
+
88
+ engine = DominoEngine(db_path=args.db, workflow_path=args.workflow)
89
+ engine.run_refactor(args.hash)
90
+
91
+
92
+ if __name__ == "__main__":
93
+ main()