superlocalmemory 2.8.2 → 2.8.5
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.
- package/ATTRIBUTION.md +1 -1
- package/CHANGELOG.md +17 -0
- package/README.md +7 -5
- package/api_server.py +5 -0
- package/bin/slm +35 -0
- package/bin/slm.bat +3 -3
- package/docs/SECURITY-QUICK-REFERENCE.md +214 -0
- package/install.ps1 +11 -11
- package/mcp_server.py +78 -10
- package/package.json +2 -2
- package/requirements-core.txt +16 -18
- package/requirements-learning.txt +8 -8
- package/requirements.txt +9 -7
- package/scripts/prepack.js +33 -0
- package/scripts/verify-v27.ps1 +301 -0
- package/src/agent_registry.py +32 -28
- package/src/auto_backup.py +12 -6
- package/src/cache_manager.py +2 -2
- package/src/compression/__init__.py +25 -0
- package/src/compression/cli.py +150 -0
- package/src/compression/cold_storage.py +217 -0
- package/src/compression/config.py +72 -0
- package/src/compression/orchestrator.py +133 -0
- package/src/compression/tier2_compressor.py +228 -0
- package/src/compression/tier3_compressor.py +153 -0
- package/src/compression/tier_classifier.py +148 -0
- package/src/db_connection_manager.py +5 -5
- package/src/event_bus.py +24 -22
- package/src/hnsw_index.py +3 -3
- package/src/learning/__init__.py +5 -4
- package/src/learning/adaptive_ranker.py +14 -265
- package/src/learning/bootstrap/__init__.py +69 -0
- package/src/learning/bootstrap/constants.py +93 -0
- package/src/learning/bootstrap/db_queries.py +316 -0
- package/src/learning/bootstrap/sampling.py +82 -0
- package/src/learning/bootstrap/text_utils.py +71 -0
- package/src/learning/cross_project_aggregator.py +58 -57
- package/src/learning/db/__init__.py +40 -0
- package/src/learning/db/constants.py +44 -0
- package/src/learning/db/schema.py +279 -0
- package/src/learning/learning_db.py +15 -234
- package/src/learning/ranking/__init__.py +33 -0
- package/src/learning/ranking/constants.py +84 -0
- package/src/learning/ranking/helpers.py +278 -0
- package/src/learning/source_quality_scorer.py +66 -65
- package/src/learning/synthetic_bootstrap.py +28 -310
- package/src/memory/__init__.py +36 -0
- package/src/memory/cli.py +205 -0
- package/src/memory/constants.py +39 -0
- package/src/memory/helpers.py +28 -0
- package/src/memory/schema.py +166 -0
- package/src/memory-profiles.py +94 -86
- package/src/memory-reset.py +187 -185
- package/src/memory_compression.py +2 -2
- package/src/memory_store_v2.py +44 -354
- package/src/migrate_v1_to_v2.py +11 -10
- package/src/patterns/analyzers.py +104 -100
- package/src/patterns/learner.py +17 -13
- package/src/patterns/scoring.py +25 -21
- package/src/patterns/store.py +40 -38
- package/src/patterns/terminology.py +53 -51
- package/src/provenance_tracker.py +2 -2
- package/src/qualixar_attribution.py +1 -1
- package/src/search/engine.py +16 -14
- package/src/search/index_loader.py +13 -11
- package/src/setup_validator.py +160 -158
- package/src/subscription_manager.py +20 -18
- package/src/tree/builder.py +66 -64
- package/src/tree/nodes.py +103 -97
- package/src/tree/queries.py +142 -137
- package/src/tree/schema.py +46 -42
- package/src/webhook_dispatcher.py +3 -3
- package/ui_server.py +7 -4
package/src/tree/nodes.py
CHANGED
|
@@ -35,46 +35,48 @@ class TreeNodesMixin:
|
|
|
35
35
|
New node ID
|
|
36
36
|
"""
|
|
37
37
|
conn = sqlite3.connect(self.db_path)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
38
|
+
try:
|
|
39
|
+
cursor = conn.cursor()
|
|
40
|
+
|
|
41
|
+
# Get parent path and depth
|
|
42
|
+
cursor.execute('SELECT tree_path, depth FROM memory_tree WHERE id = ?', (parent_id,))
|
|
43
|
+
result = cursor.fetchone()
|
|
44
|
+
|
|
45
|
+
if not result:
|
|
46
|
+
raise ValueError(f"Parent node {parent_id} not found")
|
|
47
|
+
|
|
48
|
+
parent_path, parent_depth = result
|
|
49
|
+
|
|
50
|
+
# Calculate new node position
|
|
51
|
+
depth = parent_depth + 1
|
|
52
|
+
|
|
53
|
+
cursor.execute('''
|
|
54
|
+
INSERT INTO memory_tree (
|
|
55
|
+
node_type, name, description,
|
|
56
|
+
parent_id, tree_path, depth,
|
|
57
|
+
memory_id, last_updated
|
|
58
|
+
)
|
|
59
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
60
|
+
''', (
|
|
61
|
+
node_type,
|
|
62
|
+
name,
|
|
63
|
+
description,
|
|
64
|
+
parent_id,
|
|
65
|
+
'', # Placeholder, updated below
|
|
66
|
+
depth,
|
|
67
|
+
memory_id,
|
|
68
|
+
datetime.now().isoformat()
|
|
69
|
+
))
|
|
70
|
+
|
|
71
|
+
node_id = cursor.lastrowid
|
|
72
|
+
|
|
73
|
+
# Update tree_path with actual node_id
|
|
74
|
+
tree_path = f"{parent_path}.{node_id}"
|
|
75
|
+
cursor.execute('UPDATE memory_tree SET tree_path = ? WHERE id = ?', (tree_path, node_id))
|
|
76
|
+
|
|
77
|
+
conn.commit()
|
|
78
|
+
finally:
|
|
79
|
+
conn.close()
|
|
78
80
|
|
|
79
81
|
return node_id
|
|
80
82
|
|
|
@@ -92,25 +94,26 @@ class TreeNodesMixin:
|
|
|
92
94
|
raise ValueError("Cannot delete root node")
|
|
93
95
|
|
|
94
96
|
conn = sqlite3.connect(self.db_path)
|
|
95
|
-
|
|
97
|
+
try:
|
|
98
|
+
cursor = conn.cursor()
|
|
96
99
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
+
# Get tree_path
|
|
101
|
+
cursor.execute('SELECT tree_path, parent_id FROM memory_tree WHERE id = ?', (node_id,))
|
|
102
|
+
result = cursor.fetchone()
|
|
100
103
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return False
|
|
104
|
+
if not result:
|
|
105
|
+
return False
|
|
104
106
|
|
|
105
|
-
|
|
107
|
+
tree_path, parent_id = result
|
|
106
108
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
109
|
+
# Delete node and all descendants (CASCADE handles children)
|
|
110
|
+
cursor.execute('DELETE FROM memory_tree WHERE id = ? OR tree_path LIKE ?',
|
|
111
|
+
(node_id, f"{tree_path}.%"))
|
|
110
112
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
113
|
+
deleted = cursor.rowcount > 0
|
|
114
|
+
conn.commit()
|
|
115
|
+
finally:
|
|
116
|
+
conn.close()
|
|
114
117
|
|
|
115
118
|
# Update parent counts
|
|
116
119
|
if deleted and parent_id:
|
|
@@ -127,56 +130,59 @@ class TreeNodesMixin:
|
|
|
127
130
|
node_id: Node ID to update
|
|
128
131
|
"""
|
|
129
132
|
conn = sqlite3.connect(self.db_path)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
133
|
+
try:
|
|
134
|
+
cursor = conn.cursor()
|
|
135
|
+
|
|
136
|
+
# Get all descendant memory nodes
|
|
137
|
+
cursor.execute('SELECT tree_path FROM memory_tree WHERE id = ?', (node_id,))
|
|
138
|
+
result = cursor.fetchone()
|
|
139
|
+
|
|
140
|
+
if not result:
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
tree_path = result[0]
|
|
144
|
+
|
|
145
|
+
# Count memories in subtree
|
|
146
|
+
cursor.execute('''
|
|
147
|
+
SELECT COUNT(*), COALESCE(SUM(LENGTH(m.content)), 0)
|
|
148
|
+
FROM memory_tree t
|
|
149
|
+
LEFT JOIN memories m ON t.memory_id = m.id
|
|
150
|
+
WHERE t.tree_path LIKE ? AND t.memory_id IS NOT NULL
|
|
151
|
+
''', (f"{tree_path}%",))
|
|
152
|
+
|
|
153
|
+
memory_count, total_size = cursor.fetchone()
|
|
154
|
+
|
|
155
|
+
# Update node
|
|
156
|
+
cursor.execute('''
|
|
157
|
+
UPDATE memory_tree
|
|
158
|
+
SET memory_count = ?, total_size = ?, last_updated = ?
|
|
159
|
+
WHERE id = ?
|
|
160
|
+
''', (memory_count, total_size, datetime.now().isoformat(), node_id))
|
|
161
|
+
|
|
162
|
+
# Update all ancestors
|
|
163
|
+
path_ids = [int(x) for x in tree_path.split('.')]
|
|
164
|
+
for ancestor_id in path_ids[:-1]: # Exclude current node
|
|
165
|
+
self.update_counts(ancestor_id)
|
|
166
|
+
|
|
167
|
+
conn.commit()
|
|
168
|
+
finally:
|
|
137
169
|
conn.close()
|
|
138
|
-
return
|
|
139
|
-
|
|
140
|
-
tree_path = result[0]
|
|
141
|
-
|
|
142
|
-
# Count memories in subtree
|
|
143
|
-
cursor.execute('''
|
|
144
|
-
SELECT COUNT(*), COALESCE(SUM(LENGTH(m.content)), 0)
|
|
145
|
-
FROM memory_tree t
|
|
146
|
-
LEFT JOIN memories m ON t.memory_id = m.id
|
|
147
|
-
WHERE t.tree_path LIKE ? AND t.memory_id IS NOT NULL
|
|
148
|
-
''', (f"{tree_path}%",))
|
|
149
|
-
|
|
150
|
-
memory_count, total_size = cursor.fetchone()
|
|
151
|
-
|
|
152
|
-
# Update node
|
|
153
|
-
cursor.execute('''
|
|
154
|
-
UPDATE memory_tree
|
|
155
|
-
SET memory_count = ?, total_size = ?, last_updated = ?
|
|
156
|
-
WHERE id = ?
|
|
157
|
-
''', (memory_count, total_size, datetime.now().isoformat(), node_id))
|
|
158
|
-
|
|
159
|
-
# Update all ancestors
|
|
160
|
-
path_ids = [int(x) for x in tree_path.split('.')]
|
|
161
|
-
for ancestor_id in path_ids[:-1]: # Exclude current node
|
|
162
|
-
self.update_counts(ancestor_id)
|
|
163
|
-
|
|
164
|
-
conn.commit()
|
|
165
|
-
conn.close()
|
|
166
170
|
|
|
167
171
|
def _update_all_counts(self):
|
|
168
172
|
"""Update counts for all nodes (used after build_tree)."""
|
|
169
173
|
conn = sqlite3.connect(self.db_path)
|
|
170
|
-
|
|
174
|
+
try:
|
|
175
|
+
cursor = conn.cursor()
|
|
171
176
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
+
# Get all nodes in reverse depth order (leaves first)
|
|
178
|
+
cursor.execute('''
|
|
179
|
+
SELECT id FROM memory_tree
|
|
180
|
+
ORDER BY depth DESC
|
|
181
|
+
''')
|
|
177
182
|
|
|
178
|
-
|
|
179
|
-
|
|
183
|
+
node_ids = [row[0] for row in cursor.fetchall()]
|
|
184
|
+
finally:
|
|
185
|
+
conn.close()
|
|
180
186
|
|
|
181
187
|
# Update each node (will cascade to parents)
|
|
182
188
|
processed = set()
|
package/src/tree/queries.py
CHANGED
|
@@ -28,60 +28,61 @@ class TreeQueriesMixin:
|
|
|
28
28
|
Nested dictionary representing tree structure
|
|
29
29
|
"""
|
|
30
30
|
conn = sqlite3.connect(self.db_path)
|
|
31
|
-
|
|
31
|
+
try:
|
|
32
|
+
cursor = conn.cursor()
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
cursor.execute('''
|
|
37
|
-
SELECT id, tree_path FROM memory_tree
|
|
38
|
-
WHERE node_type = 'project' AND name = ?
|
|
39
|
-
''', (project_name,))
|
|
40
|
-
result = cursor.fetchone()
|
|
41
|
-
|
|
42
|
-
if not result:
|
|
43
|
-
conn.close()
|
|
44
|
-
return {'error': f"Project '{project_name}' not found"}
|
|
45
|
-
|
|
46
|
-
project_id, project_path = result
|
|
47
|
-
|
|
48
|
-
# Get subtree
|
|
49
|
-
if max_depth is not None:
|
|
34
|
+
# Build query
|
|
35
|
+
if project_name:
|
|
36
|
+
# Find project node
|
|
50
37
|
cursor.execute('''
|
|
51
|
-
SELECT id,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
38
|
+
SELECT id, tree_path FROM memory_tree
|
|
39
|
+
WHERE node_type = 'project' AND name = ?
|
|
40
|
+
''', (project_name,))
|
|
41
|
+
result = cursor.fetchone()
|
|
42
|
+
|
|
43
|
+
if not result:
|
|
44
|
+
return {'error': f"Project '{project_name}' not found"}
|
|
45
|
+
|
|
46
|
+
project_id, project_path = result
|
|
47
|
+
|
|
48
|
+
# Get subtree
|
|
49
|
+
if max_depth is not None:
|
|
50
|
+
cursor.execute('''
|
|
51
|
+
SELECT id, node_type, name, description, parent_id, tree_path,
|
|
52
|
+
depth, memory_count, total_size, last_updated, memory_id
|
|
53
|
+
FROM memory_tree
|
|
54
|
+
WHERE (id = ? OR tree_path LIKE ?) AND depth <= ?
|
|
55
|
+
ORDER BY tree_path
|
|
56
|
+
''', (project_id, f"{project_path}.%", max_depth))
|
|
57
|
+
else:
|
|
58
|
+
cursor.execute('''
|
|
59
|
+
SELECT id, node_type, name, description, parent_id, tree_path,
|
|
60
|
+
depth, memory_count, total_size, last_updated, memory_id
|
|
61
|
+
FROM memory_tree
|
|
62
|
+
WHERE id = ? OR tree_path LIKE ?
|
|
63
|
+
ORDER BY tree_path
|
|
64
|
+
''', (project_id, f"{project_path}.%"))
|
|
57
65
|
else:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
depth, memory_count, total_size, last_updated, memory_id
|
|
79
|
-
FROM memory_tree
|
|
80
|
-
ORDER BY tree_path
|
|
81
|
-
''')
|
|
82
|
-
|
|
83
|
-
rows = cursor.fetchall()
|
|
84
|
-
conn.close()
|
|
66
|
+
# Get entire tree
|
|
67
|
+
if max_depth is not None:
|
|
68
|
+
cursor.execute('''
|
|
69
|
+
SELECT id, node_type, name, description, parent_id, tree_path,
|
|
70
|
+
depth, memory_count, total_size, last_updated, memory_id
|
|
71
|
+
FROM memory_tree
|
|
72
|
+
WHERE depth <= ?
|
|
73
|
+
ORDER BY tree_path
|
|
74
|
+
''', (max_depth,))
|
|
75
|
+
else:
|
|
76
|
+
cursor.execute('''
|
|
77
|
+
SELECT id, node_type, name, description, parent_id, tree_path,
|
|
78
|
+
depth, memory_count, total_size, last_updated, memory_id
|
|
79
|
+
FROM memory_tree
|
|
80
|
+
ORDER BY tree_path
|
|
81
|
+
''')
|
|
82
|
+
|
|
83
|
+
rows = cursor.fetchall()
|
|
84
|
+
finally:
|
|
85
|
+
conn.close()
|
|
85
86
|
|
|
86
87
|
if not rows:
|
|
87
88
|
return {'error': 'No tree nodes found'}
|
|
@@ -125,44 +126,45 @@ class TreeQueriesMixin:
|
|
|
125
126
|
List of descendant nodes
|
|
126
127
|
"""
|
|
127
128
|
conn = sqlite3.connect(self.db_path)
|
|
128
|
-
|
|
129
|
+
try:
|
|
130
|
+
cursor = conn.cursor()
|
|
131
|
+
|
|
132
|
+
# Get node's tree_path
|
|
133
|
+
cursor.execute('SELECT tree_path FROM memory_tree WHERE id = ?', (node_id,))
|
|
134
|
+
result = cursor.fetchone()
|
|
135
|
+
|
|
136
|
+
if not result:
|
|
137
|
+
return []
|
|
129
138
|
|
|
130
|
-
|
|
131
|
-
cursor.execute('SELECT tree_path FROM memory_tree WHERE id = ?', (node_id,))
|
|
132
|
-
result = cursor.fetchone()
|
|
139
|
+
tree_path = result[0]
|
|
133
140
|
|
|
134
|
-
|
|
141
|
+
# Get all descendants
|
|
142
|
+
cursor.execute('''
|
|
143
|
+
SELECT id, node_type, name, description, parent_id, tree_path,
|
|
144
|
+
depth, memory_count, total_size, last_updated, memory_id
|
|
145
|
+
FROM memory_tree
|
|
146
|
+
WHERE tree_path LIKE ?
|
|
147
|
+
ORDER BY tree_path
|
|
148
|
+
''', (f"{tree_path}.%",))
|
|
149
|
+
|
|
150
|
+
results = []
|
|
151
|
+
for row in cursor.fetchall():
|
|
152
|
+
results.append({
|
|
153
|
+
'id': row[0],
|
|
154
|
+
'type': row[1],
|
|
155
|
+
'name': row[2],
|
|
156
|
+
'description': row[3],
|
|
157
|
+
'parent_id': row[4],
|
|
158
|
+
'tree_path': row[5],
|
|
159
|
+
'depth': row[6],
|
|
160
|
+
'memory_count': row[7],
|
|
161
|
+
'total_size': row[8],
|
|
162
|
+
'last_updated': row[9],
|
|
163
|
+
'memory_id': row[10]
|
|
164
|
+
})
|
|
165
|
+
finally:
|
|
135
166
|
conn.close()
|
|
136
|
-
return []
|
|
137
|
-
|
|
138
|
-
tree_path = result[0]
|
|
139
|
-
|
|
140
|
-
# Get all descendants
|
|
141
|
-
cursor.execute('''
|
|
142
|
-
SELECT id, node_type, name, description, parent_id, tree_path,
|
|
143
|
-
depth, memory_count, total_size, last_updated, memory_id
|
|
144
|
-
FROM memory_tree
|
|
145
|
-
WHERE tree_path LIKE ?
|
|
146
|
-
ORDER BY tree_path
|
|
147
|
-
''', (f"{tree_path}.%",))
|
|
148
|
-
|
|
149
|
-
results = []
|
|
150
|
-
for row in cursor.fetchall():
|
|
151
|
-
results.append({
|
|
152
|
-
'id': row[0],
|
|
153
|
-
'type': row[1],
|
|
154
|
-
'name': row[2],
|
|
155
|
-
'description': row[3],
|
|
156
|
-
'parent_id': row[4],
|
|
157
|
-
'tree_path': row[5],
|
|
158
|
-
'depth': row[6],
|
|
159
|
-
'memory_count': row[7],
|
|
160
|
-
'total_size': row[8],
|
|
161
|
-
'last_updated': row[9],
|
|
162
|
-
'memory_id': row[10]
|
|
163
|
-
})
|
|
164
167
|
|
|
165
|
-
conn.close()
|
|
166
168
|
return results
|
|
167
169
|
|
|
168
170
|
def get_path_to_root(self, node_id: int) -> List[Dict[str, Any]]:
|
|
@@ -176,72 +178,75 @@ class TreeQueriesMixin:
|
|
|
176
178
|
List of nodes from root to target node
|
|
177
179
|
"""
|
|
178
180
|
conn = sqlite3.connect(self.db_path)
|
|
179
|
-
|
|
181
|
+
try:
|
|
182
|
+
cursor = conn.cursor()
|
|
180
183
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
+
# Get node's tree_path
|
|
185
|
+
cursor.execute('SELECT tree_path FROM memory_tree WHERE id = ?', (node_id,))
|
|
186
|
+
result = cursor.fetchone()
|
|
184
187
|
|
|
185
|
-
|
|
188
|
+
if not result:
|
|
189
|
+
return []
|
|
190
|
+
|
|
191
|
+
tree_path = result[0]
|
|
192
|
+
|
|
193
|
+
# Parse path to get all ancestor IDs
|
|
194
|
+
path_ids = [int(x) for x in tree_path.split('.')]
|
|
195
|
+
|
|
196
|
+
# Get all ancestor nodes
|
|
197
|
+
placeholders = ','.join('?' * len(path_ids))
|
|
198
|
+
cursor.execute(f'''
|
|
199
|
+
SELECT id, node_type, name, description, parent_id, tree_path,
|
|
200
|
+
depth, memory_count, total_size, last_updated, memory_id
|
|
201
|
+
FROM memory_tree
|
|
202
|
+
WHERE id IN ({placeholders})
|
|
203
|
+
ORDER BY depth
|
|
204
|
+
''', path_ids)
|
|
205
|
+
|
|
206
|
+
results = []
|
|
207
|
+
for row in cursor.fetchall():
|
|
208
|
+
results.append({
|
|
209
|
+
'id': row[0],
|
|
210
|
+
'type': row[1],
|
|
211
|
+
'name': row[2],
|
|
212
|
+
'description': row[3],
|
|
213
|
+
'parent_id': row[4],
|
|
214
|
+
'tree_path': row[5],
|
|
215
|
+
'depth': row[6],
|
|
216
|
+
'memory_count': row[7],
|
|
217
|
+
'total_size': row[8],
|
|
218
|
+
'last_updated': row[9],
|
|
219
|
+
'memory_id': row[10]
|
|
220
|
+
})
|
|
221
|
+
finally:
|
|
186
222
|
conn.close()
|
|
187
|
-
return []
|
|
188
|
-
|
|
189
|
-
tree_path = result[0]
|
|
190
|
-
|
|
191
|
-
# Parse path to get all ancestor IDs
|
|
192
|
-
path_ids = [int(x) for x in tree_path.split('.')]
|
|
193
|
-
|
|
194
|
-
# Get all ancestor nodes
|
|
195
|
-
placeholders = ','.join('?' * len(path_ids))
|
|
196
|
-
cursor.execute(f'''
|
|
197
|
-
SELECT id, node_type, name, description, parent_id, tree_path,
|
|
198
|
-
depth, memory_count, total_size, last_updated, memory_id
|
|
199
|
-
FROM memory_tree
|
|
200
|
-
WHERE id IN ({placeholders})
|
|
201
|
-
ORDER BY depth
|
|
202
|
-
''', path_ids)
|
|
203
|
-
|
|
204
|
-
results = []
|
|
205
|
-
for row in cursor.fetchall():
|
|
206
|
-
results.append({
|
|
207
|
-
'id': row[0],
|
|
208
|
-
'type': row[1],
|
|
209
|
-
'name': row[2],
|
|
210
|
-
'description': row[3],
|
|
211
|
-
'parent_id': row[4],
|
|
212
|
-
'tree_path': row[5],
|
|
213
|
-
'depth': row[6],
|
|
214
|
-
'memory_count': row[7],
|
|
215
|
-
'total_size': row[8],
|
|
216
|
-
'last_updated': row[9],
|
|
217
|
-
'memory_id': row[10]
|
|
218
|
-
})
|
|
219
223
|
|
|
220
|
-
conn.close()
|
|
221
224
|
return results
|
|
222
225
|
|
|
223
226
|
def get_stats(self) -> Dict[str, Any]:
|
|
224
227
|
"""Get tree statistics."""
|
|
225
228
|
conn = sqlite3.connect(self.db_path)
|
|
226
|
-
|
|
229
|
+
try:
|
|
230
|
+
cursor = conn.cursor()
|
|
227
231
|
|
|
228
|
-
|
|
229
|
-
|
|
232
|
+
cursor.execute('SELECT COUNT(*) FROM memory_tree')
|
|
233
|
+
total_nodes = cursor.fetchone()[0]
|
|
230
234
|
|
|
231
|
-
|
|
232
|
-
|
|
235
|
+
cursor.execute('SELECT node_type, COUNT(*) FROM memory_tree GROUP BY node_type')
|
|
236
|
+
by_type = dict(cursor.fetchall())
|
|
233
237
|
|
|
234
|
-
|
|
235
|
-
|
|
238
|
+
cursor.execute('SELECT MAX(depth) FROM memory_tree')
|
|
239
|
+
max_depth = cursor.fetchone()[0] or 0
|
|
236
240
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
241
|
+
cursor.execute('''
|
|
242
|
+
SELECT SUM(memory_count), SUM(total_size)
|
|
243
|
+
FROM memory_tree
|
|
244
|
+
WHERE node_type = 'root'
|
|
245
|
+
''')
|
|
246
|
+
total_memories, total_size = cursor.fetchone()
|
|
243
247
|
|
|
244
|
-
|
|
248
|
+
finally:
|
|
249
|
+
conn.close()
|
|
245
250
|
|
|
246
251
|
return {
|
|
247
252
|
'total_nodes': total_nodes,
|
package/src/tree/schema.py
CHANGED
|
@@ -20,57 +20,61 @@ class TreeSchemaMixin:
|
|
|
20
20
|
def _init_db(self):
|
|
21
21
|
"""Initialize memory_tree table."""
|
|
22
22
|
conn = sqlite3.connect(self.db_path)
|
|
23
|
-
|
|
23
|
+
try:
|
|
24
|
+
cursor = conn.cursor()
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
cursor.execute('''
|
|
27
|
+
CREATE TABLE IF NOT EXISTS memory_tree (
|
|
28
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
29
|
+
node_type TEXT NOT NULL,
|
|
30
|
+
name TEXT NOT NULL,
|
|
31
|
+
description TEXT,
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
parent_id INTEGER,
|
|
34
|
+
tree_path TEXT NOT NULL,
|
|
35
|
+
depth INTEGER DEFAULT 0,
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
memory_count INTEGER DEFAULT 0,
|
|
38
|
+
total_size INTEGER DEFAULT 0,
|
|
39
|
+
last_updated TIMESTAMP,
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
memory_id INTEGER,
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
FOREIGN KEY (parent_id) REFERENCES memory_tree(id) ON DELETE CASCADE,
|
|
44
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
|
|
45
|
+
)
|
|
46
|
+
''')
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
cursor.execute('CREATE INDEX IF NOT EXISTS idx_tree_path_layer2 ON memory_tree(tree_path)')
|
|
49
|
+
cursor.execute('CREATE INDEX IF NOT EXISTS idx_node_type ON memory_tree(node_type)')
|
|
50
|
+
cursor.execute('CREATE INDEX IF NOT EXISTS idx_parent_id_tree ON memory_tree(parent_id)')
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
conn.commit()
|
|
53
|
+
finally:
|
|
54
|
+
conn.close()
|
|
53
55
|
|
|
54
56
|
def _ensure_root(self) -> int:
|
|
55
57
|
"""Ensure root node exists and return its ID."""
|
|
56
58
|
conn = sqlite3.connect(self.db_path)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
59
|
+
try:
|
|
60
|
+
cursor = conn.cursor()
|
|
61
|
+
|
|
62
|
+
cursor.execute('SELECT id FROM memory_tree WHERE node_type = ? AND parent_id IS NULL', ('root',))
|
|
63
|
+
result = cursor.fetchone()
|
|
64
|
+
|
|
65
|
+
if result:
|
|
66
|
+
root_id = result[0]
|
|
67
|
+
else:
|
|
68
|
+
cursor.execute('''
|
|
69
|
+
INSERT INTO memory_tree (node_type, name, tree_path, depth, last_updated)
|
|
70
|
+
VALUES (?, ?, ?, ?, ?)
|
|
71
|
+
''', ('root', 'Root', '1', 0, datetime.now().isoformat()))
|
|
72
|
+
root_id = cursor.lastrowid
|
|
73
|
+
|
|
74
|
+
# Update tree_path with actual ID
|
|
75
|
+
cursor.execute('UPDATE memory_tree SET tree_path = ? WHERE id = ?', (str(root_id), root_id))
|
|
76
|
+
conn.commit()
|
|
77
|
+
|
|
78
|
+
finally:
|
|
79
|
+
conn.close()
|
|
76
80
|
return root_id
|