agmem 0.1.1__py3-none-any.whl → 0.1.2__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.
- {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/METADATA +20 -3
- agmem-0.1.2.dist-info/RECORD +86 -0
- memvcs/__init__.py +1 -1
- memvcs/cli.py +35 -31
- memvcs/commands/__init__.py +9 -9
- memvcs/commands/add.py +77 -76
- memvcs/commands/blame.py +46 -53
- memvcs/commands/branch.py +13 -33
- memvcs/commands/checkout.py +27 -32
- memvcs/commands/clean.py +18 -23
- memvcs/commands/clone.py +4 -1
- memvcs/commands/commit.py +40 -39
- memvcs/commands/daemon.py +81 -76
- memvcs/commands/decay.py +77 -0
- memvcs/commands/diff.py +56 -57
- memvcs/commands/distill.py +74 -0
- memvcs/commands/fsck.py +55 -61
- memvcs/commands/garden.py +28 -37
- memvcs/commands/graph.py +41 -48
- memvcs/commands/init.py +16 -24
- memvcs/commands/log.py +25 -40
- memvcs/commands/merge.py +16 -28
- memvcs/commands/pack.py +129 -0
- memvcs/commands/pull.py +4 -1
- memvcs/commands/push.py +4 -2
- memvcs/commands/recall.py +145 -0
- memvcs/commands/reflog.py +13 -22
- memvcs/commands/remote.py +1 -0
- memvcs/commands/repair.py +66 -0
- memvcs/commands/reset.py +23 -33
- memvcs/commands/resurrect.py +82 -0
- memvcs/commands/search.py +3 -4
- memvcs/commands/serve.py +2 -1
- memvcs/commands/show.py +66 -36
- memvcs/commands/stash.py +34 -34
- memvcs/commands/status.py +27 -35
- memvcs/commands/tag.py +23 -47
- memvcs/commands/test.py +30 -44
- memvcs/commands/timeline.py +111 -0
- memvcs/commands/tree.py +26 -27
- memvcs/commands/verify.py +59 -0
- memvcs/commands/when.py +115 -0
- memvcs/core/access_index.py +167 -0
- memvcs/core/config_loader.py +3 -1
- memvcs/core/consistency.py +214 -0
- memvcs/core/decay.py +185 -0
- memvcs/core/diff.py +158 -143
- memvcs/core/distiller.py +277 -0
- memvcs/core/gardener.py +164 -132
- memvcs/core/hooks.py +48 -14
- memvcs/core/knowledge_graph.py +134 -138
- memvcs/core/merge.py +248 -171
- memvcs/core/objects.py +95 -96
- memvcs/core/pii_scanner.py +147 -146
- memvcs/core/refs.py +132 -115
- memvcs/core/repository.py +174 -164
- memvcs/core/schema.py +155 -113
- memvcs/core/staging.py +60 -65
- memvcs/core/storage/__init__.py +20 -18
- memvcs/core/storage/base.py +74 -70
- memvcs/core/storage/gcs.py +70 -68
- memvcs/core/storage/local.py +42 -40
- memvcs/core/storage/s3.py +105 -110
- memvcs/core/temporal_index.py +112 -0
- memvcs/core/test_runner.py +101 -93
- memvcs/core/vector_store.py +41 -35
- memvcs/integrations/mcp_server.py +1 -3
- memvcs/integrations/web_ui/server.py +25 -26
- memvcs/retrieval/__init__.py +22 -0
- memvcs/retrieval/base.py +54 -0
- memvcs/retrieval/pack.py +128 -0
- memvcs/retrieval/recaller.py +105 -0
- memvcs/retrieval/strategies.py +314 -0
- memvcs/utils/__init__.py +3 -3
- memvcs/utils/helpers.py +52 -52
- agmem-0.1.1.dist-info/RECORD +0 -67
- {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/WHEEL +0 -0
- {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/entry_points.txt +0 -0
- {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/licenses/LICENSE +0 -0
- {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/top_level.txt +0 -0
memvcs/commands/tag.py
CHANGED
|
@@ -13,42 +13,19 @@ from ..core.repository import Repository
|
|
|
13
13
|
|
|
14
14
|
class TagCommand:
|
|
15
15
|
"""Manage tags."""
|
|
16
|
-
|
|
17
|
-
name =
|
|
18
|
-
help =
|
|
19
|
-
|
|
16
|
+
|
|
17
|
+
name = "tag"
|
|
18
|
+
help = "Create, list, delete or verify a tag"
|
|
19
|
+
|
|
20
20
|
@staticmethod
|
|
21
21
|
def add_arguments(parser: argparse.ArgumentParser):
|
|
22
|
-
parser.add_argument(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
)
|
|
27
|
-
parser.add_argument(
|
|
28
|
-
|
|
29
|
-
nargs='?',
|
|
30
|
-
help='Commit to tag (default: HEAD)'
|
|
31
|
-
)
|
|
32
|
-
parser.add_argument(
|
|
33
|
-
'--list', '-l',
|
|
34
|
-
action='store_true',
|
|
35
|
-
help='List tags'
|
|
36
|
-
)
|
|
37
|
-
parser.add_argument(
|
|
38
|
-
'--delete', '-d',
|
|
39
|
-
action='store_true',
|
|
40
|
-
help='Delete a tag'
|
|
41
|
-
)
|
|
42
|
-
parser.add_argument(
|
|
43
|
-
'-m', '--message',
|
|
44
|
-
help='Tag message'
|
|
45
|
-
)
|
|
46
|
-
parser.add_argument(
|
|
47
|
-
'--force', '-f',
|
|
48
|
-
action='store_true',
|
|
49
|
-
help='Force replace existing tag'
|
|
50
|
-
)
|
|
51
|
-
|
|
22
|
+
parser.add_argument("name", nargs="?", help="Tag name")
|
|
23
|
+
parser.add_argument("commit", nargs="?", help="Commit to tag (default: HEAD)")
|
|
24
|
+
parser.add_argument("--list", "-l", action="store_true", help="List tags")
|
|
25
|
+
parser.add_argument("--delete", "-d", action="store_true", help="Delete a tag")
|
|
26
|
+
parser.add_argument("-m", "--message", help="Tag message")
|
|
27
|
+
parser.add_argument("--force", "-f", action="store_true", help="Force replace existing tag")
|
|
28
|
+
|
|
52
29
|
@staticmethod
|
|
53
30
|
def execute(args) -> int:
|
|
54
31
|
# Find repository
|
|
@@ -56,35 +33,34 @@ class TagCommand:
|
|
|
56
33
|
if code != 0:
|
|
57
34
|
return code
|
|
58
35
|
|
|
59
|
-
|
|
60
36
|
# List tags
|
|
61
37
|
if args.list or (not args.name and not args.delete):
|
|
62
38
|
tags = repo.refs.list_tags()
|
|
63
|
-
|
|
39
|
+
|
|
64
40
|
if not tags:
|
|
65
41
|
print("No tags yet.")
|
|
66
42
|
return 0
|
|
67
|
-
|
|
43
|
+
|
|
68
44
|
for tag in sorted(tags):
|
|
69
45
|
commit_hash = repo.refs.get_tag_commit(tag)
|
|
70
46
|
short_hash = commit_hash[:8] if commit_hash else "????????"
|
|
71
47
|
print(f"{tag}\t{short_hash}")
|
|
72
|
-
|
|
48
|
+
|
|
73
49
|
return 0
|
|
74
|
-
|
|
50
|
+
|
|
75
51
|
# Delete tag
|
|
76
52
|
if args.delete:
|
|
77
53
|
if not args.name:
|
|
78
54
|
print("Error: Tag name required for deletion")
|
|
79
55
|
return 1
|
|
80
|
-
|
|
56
|
+
|
|
81
57
|
if repo.refs.delete_tag(args.name):
|
|
82
58
|
print(f"Deleted tag '{args.name}'")
|
|
83
59
|
return 0
|
|
84
60
|
else:
|
|
85
61
|
print(f"Error: Tag '{args.name}' not found")
|
|
86
62
|
return 1
|
|
87
|
-
|
|
63
|
+
|
|
88
64
|
# Create tag
|
|
89
65
|
if args.name:
|
|
90
66
|
# Check if tag exists
|
|
@@ -92,19 +68,19 @@ class TagCommand:
|
|
|
92
68
|
print(f"Error: Tag '{args.name}' already exists")
|
|
93
69
|
print("Use -f to force replace")
|
|
94
70
|
return 1
|
|
95
|
-
|
|
71
|
+
|
|
96
72
|
# Get commit to tag
|
|
97
|
-
commit_ref = args.commit or
|
|
73
|
+
commit_ref = args.commit or "HEAD"
|
|
98
74
|
commit_hash = repo.resolve_ref(commit_ref)
|
|
99
|
-
|
|
75
|
+
|
|
100
76
|
if not commit_hash:
|
|
101
77
|
print(f"Error: Unknown revision: {commit_ref}")
|
|
102
78
|
return 1
|
|
103
|
-
|
|
79
|
+
|
|
104
80
|
# Delete existing tag if forcing
|
|
105
81
|
if args.force and repo.refs.tag_exists(args.name):
|
|
106
82
|
repo.refs.delete_tag(args.name)
|
|
107
|
-
|
|
83
|
+
|
|
108
84
|
# Create tag
|
|
109
85
|
message = args.message or f"Tag {args.name}"
|
|
110
86
|
if repo.refs.create_tag(args.name, commit_hash, message):
|
|
@@ -113,5 +89,5 @@ class TagCommand:
|
|
|
113
89
|
else:
|
|
114
90
|
print(f"Error: Could not create tag '{args.name}'")
|
|
115
91
|
return 1
|
|
116
|
-
|
|
92
|
+
|
|
117
93
|
return 0
|
memvcs/commands/test.py
CHANGED
|
@@ -11,80 +11,66 @@ from ..core.test_runner import TestRunner, create_test_template
|
|
|
11
11
|
|
|
12
12
|
class TestCommand:
|
|
13
13
|
"""Run memory regression tests."""
|
|
14
|
-
|
|
15
|
-
name =
|
|
16
|
-
help =
|
|
17
|
-
|
|
14
|
+
|
|
15
|
+
name = "test"
|
|
16
|
+
help = "Run memory regression tests to validate knowledge consistency"
|
|
17
|
+
|
|
18
18
|
@staticmethod
|
|
19
19
|
def add_arguments(parser: argparse.ArgumentParser):
|
|
20
|
+
parser.add_argument("--branch", help="Run tests against a specific branch")
|
|
21
|
+
parser.add_argument("--tags", nargs="+", help="Filter tests by tags")
|
|
20
22
|
parser.add_argument(
|
|
21
|
-
|
|
22
|
-
help='Run tests against a specific branch'
|
|
23
|
-
)
|
|
24
|
-
parser.add_argument(
|
|
25
|
-
'--tags',
|
|
26
|
-
nargs='+',
|
|
27
|
-
help='Filter tests by tags'
|
|
23
|
+
"--init", action="store_true", help="Initialize tests directory with template"
|
|
28
24
|
)
|
|
29
25
|
parser.add_argument(
|
|
30
|
-
|
|
31
|
-
action='store_true',
|
|
32
|
-
help='Initialize tests directory with template'
|
|
26
|
+
"-v", "--verbose", action="store_true", help="Show detailed test output"
|
|
33
27
|
)
|
|
34
|
-
parser.add_argument(
|
|
35
|
-
|
|
36
|
-
action='store_true',
|
|
37
|
-
help='Show detailed test output'
|
|
38
|
-
)
|
|
39
|
-
parser.add_argument(
|
|
40
|
-
'--fail-fast',
|
|
41
|
-
action='store_true',
|
|
42
|
-
help='Stop on first failure'
|
|
43
|
-
)
|
|
44
|
-
|
|
28
|
+
parser.add_argument("--fail-fast", action="store_true", help="Stop on first failure")
|
|
29
|
+
|
|
45
30
|
@staticmethod
|
|
46
31
|
def execute(args) -> int:
|
|
47
32
|
repo, code = require_repo()
|
|
48
33
|
if code != 0:
|
|
49
34
|
return code
|
|
50
|
-
|
|
35
|
+
|
|
51
36
|
# Handle --init
|
|
52
37
|
if args.init:
|
|
53
38
|
return TestCommand._init_tests(repo)
|
|
54
|
-
|
|
39
|
+
|
|
55
40
|
# Try to get vector store
|
|
56
41
|
vector_store = None
|
|
57
42
|
try:
|
|
58
43
|
from ..core.vector_store import VectorStore
|
|
59
|
-
|
|
44
|
+
|
|
45
|
+
vector_store = VectorStore(repo.root / ".mem")
|
|
60
46
|
except ImportError:
|
|
61
47
|
if args.verbose:
|
|
62
48
|
print("Note: Vector store not available, using text-based tests")
|
|
63
49
|
except Exception as e:
|
|
64
50
|
if args.verbose:
|
|
65
51
|
print(f"Note: Could not initialize vector store: {e}")
|
|
66
|
-
|
|
52
|
+
|
|
67
53
|
# Create test runner
|
|
68
54
|
runner = TestRunner(repo, vector_store)
|
|
69
|
-
|
|
55
|
+
|
|
70
56
|
# Load and check for tests
|
|
71
57
|
tests = runner.load_tests()
|
|
72
58
|
if not tests:
|
|
73
59
|
print("No tests found.")
|
|
74
60
|
print("Create test files in tests/ directory or run 'agmem test --init'")
|
|
75
61
|
return 0
|
|
76
|
-
|
|
62
|
+
|
|
77
63
|
print(f"Running {len(tests)} tests...")
|
|
78
|
-
|
|
64
|
+
|
|
79
65
|
# Run tests
|
|
80
66
|
if args.branch:
|
|
81
67
|
result = runner.run_for_branch(args.branch)
|
|
82
68
|
else:
|
|
83
69
|
result = runner.run_all(tags=args.tags)
|
|
84
|
-
|
|
70
|
+
|
|
85
71
|
# Print results
|
|
86
72
|
print()
|
|
87
|
-
|
|
73
|
+
|
|
88
74
|
if result.failures:
|
|
89
75
|
print("Failed tests:")
|
|
90
76
|
for failure in result.failures:
|
|
@@ -97,11 +83,11 @@ class TestCommand:
|
|
|
97
83
|
print(f" Got: {failure.actual[:100]}...")
|
|
98
84
|
print(f" Error: {failure.message}")
|
|
99
85
|
print()
|
|
100
|
-
|
|
86
|
+
|
|
101
87
|
# Summary
|
|
102
88
|
status = "PASSED" if result.passed else "FAILED"
|
|
103
89
|
critical_failures = [f for f in result.failures if f.is_critical]
|
|
104
|
-
|
|
90
|
+
|
|
105
91
|
print(f"{'='*50}")
|
|
106
92
|
print(f"Results: {result.passed_count}/{result.total_count} tests passed")
|
|
107
93
|
if critical_failures:
|
|
@@ -109,24 +95,24 @@ class TestCommand:
|
|
|
109
95
|
print(f"Duration: {result.duration_ms}ms")
|
|
110
96
|
print(f"Status: {status}")
|
|
111
97
|
print(f"{'='*50}")
|
|
112
|
-
|
|
98
|
+
|
|
113
99
|
return 0 if result.passed else 1
|
|
114
|
-
|
|
100
|
+
|
|
115
101
|
@staticmethod
|
|
116
102
|
def _init_tests(repo) -> int:
|
|
117
103
|
"""Initialize tests directory with template."""
|
|
118
|
-
tests_dir = repo.root /
|
|
104
|
+
tests_dir = repo.root / "tests"
|
|
119
105
|
tests_dir.mkdir(exist_ok=True)
|
|
120
|
-
|
|
121
|
-
template_file = tests_dir /
|
|
122
|
-
|
|
106
|
+
|
|
107
|
+
template_file = tests_dir / "example_tests.yaml"
|
|
108
|
+
|
|
123
109
|
if template_file.exists():
|
|
124
110
|
print(f"Test template already exists: {template_file}")
|
|
125
111
|
return 0
|
|
126
|
-
|
|
112
|
+
|
|
127
113
|
template_file.write_text(create_test_template())
|
|
128
114
|
print(f"Created test template: {template_file}")
|
|
129
115
|
print("\nEdit this file to add your memory tests.")
|
|
130
116
|
print("Run 'agmem test' to execute them.")
|
|
131
|
-
|
|
117
|
+
|
|
132
118
|
return 0
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agmem timeline - Show evolution of a specific memory file over time.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from ..commands.base import require_repo
|
|
9
|
+
from ..core.objects import Commit, Tree, Blob
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TimelineCommand:
|
|
13
|
+
"""Show evolution of a memory file (blame-style over time)."""
|
|
14
|
+
|
|
15
|
+
name = "timeline"
|
|
16
|
+
help = "Show evolution of a specific memory file over time"
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def add_arguments(parser: argparse.ArgumentParser):
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"file",
|
|
22
|
+
help="File to show timeline for (path relative to current/)",
|
|
23
|
+
)
|
|
24
|
+
parser.add_argument(
|
|
25
|
+
"--limit",
|
|
26
|
+
"-n",
|
|
27
|
+
type=int,
|
|
28
|
+
default=20,
|
|
29
|
+
help="Max commits to show (default: 20)",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def execute(args) -> int:
|
|
34
|
+
repo, code = require_repo()
|
|
35
|
+
if code != 0:
|
|
36
|
+
return code
|
|
37
|
+
|
|
38
|
+
filepath = args.file.replace("current/", "").lstrip("/")
|
|
39
|
+
|
|
40
|
+
# Walk commit history
|
|
41
|
+
head = repo.refs.get_head()
|
|
42
|
+
commit_hash = (
|
|
43
|
+
repo.refs.get_branch_commit(head["value"])
|
|
44
|
+
if head["type"] == "branch"
|
|
45
|
+
else head.get("value")
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
history = []
|
|
49
|
+
seen = set()
|
|
50
|
+
while commit_hash and len(history) < args.limit:
|
|
51
|
+
if commit_hash in seen:
|
|
52
|
+
break
|
|
53
|
+
seen.add(commit_hash)
|
|
54
|
+
|
|
55
|
+
commit = Commit.load(repo.object_store, commit_hash)
|
|
56
|
+
if not commit:
|
|
57
|
+
break
|
|
58
|
+
|
|
59
|
+
tree = repo.get_commit_tree(commit_hash)
|
|
60
|
+
if not tree:
|
|
61
|
+
commit_hash = commit.parents[0] if commit.parents else None
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
blob_hash = None
|
|
65
|
+
for entry in tree.entries:
|
|
66
|
+
path = entry.path + "/" + entry.name if entry.path else entry.name
|
|
67
|
+
if path == filepath:
|
|
68
|
+
blob_hash = entry.hash
|
|
69
|
+
break
|
|
70
|
+
|
|
71
|
+
if blob_hash:
|
|
72
|
+
blob = Blob.load(repo.object_store, blob_hash)
|
|
73
|
+
content = blob.content.decode("utf-8", errors="replace") if blob else ""
|
|
74
|
+
history.append(
|
|
75
|
+
{
|
|
76
|
+
"commit": commit_hash,
|
|
77
|
+
"timestamp": commit.timestamp,
|
|
78
|
+
"author": commit.author,
|
|
79
|
+
"message": commit.message,
|
|
80
|
+
"content": content,
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
commit_hash = commit.parents[0] if commit.parents else None
|
|
85
|
+
|
|
86
|
+
if not history:
|
|
87
|
+
print(f"File {filepath} not found in commit history.")
|
|
88
|
+
return 1
|
|
89
|
+
|
|
90
|
+
print(f"Timeline for {filepath}:")
|
|
91
|
+
print("=" * 60)
|
|
92
|
+
for i, h in enumerate(history):
|
|
93
|
+
print(f"\n[{i + 1}] {h['commit'][:8]} {h['timestamp']}")
|
|
94
|
+
print(f" {h['author']}")
|
|
95
|
+
print(f" {h['message'][:70]}")
|
|
96
|
+
if i > 0 and history[i - 1]["content"] != h["content"]:
|
|
97
|
+
prev_content = history[i - 1]["content"].encode()
|
|
98
|
+
curr_content = h["content"].encode()
|
|
99
|
+
# Simple line diff
|
|
100
|
+
prev_lines = prev_content.splitlines()
|
|
101
|
+
curr_lines = curr_content.splitlines()
|
|
102
|
+
for j, (a, b) in enumerate(zip(prev_lines, curr_lines)):
|
|
103
|
+
if a != b:
|
|
104
|
+
print(f" ... (changed at line {j + 1})")
|
|
105
|
+
break
|
|
106
|
+
else:
|
|
107
|
+
if len(prev_lines) != len(curr_lines):
|
|
108
|
+
print(f" ... (lines changed: {len(prev_lines)} -> {len(curr_lines)})")
|
|
109
|
+
print()
|
|
110
|
+
|
|
111
|
+
return 0
|
memvcs/commands/tree.py
CHANGED
|
@@ -4,7 +4,7 @@ agmem tree - Show working directory or commit tree visually.
|
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import Optional
|
|
7
|
+
from typing import List, Optional
|
|
8
8
|
|
|
9
9
|
from ..commands.base import require_repo
|
|
10
10
|
from ..core.objects import Commit, Tree
|
|
@@ -18,7 +18,7 @@ def _build_tree_lines(
|
|
|
18
18
|
show_hidden: bool = False,
|
|
19
19
|
depth_limit: Optional[int] = None,
|
|
20
20
|
current_depth: int = 0,
|
|
21
|
-
) ->
|
|
21
|
+
) -> List[str]:
|
|
22
22
|
"""Build tree lines for a directory."""
|
|
23
23
|
lines = []
|
|
24
24
|
if depth_limit is not None and current_depth >= depth_limit:
|
|
@@ -27,33 +27,32 @@ def _build_tree_lines(
|
|
|
27
27
|
entries = sorted(base_path.iterdir(), key=lambda p: (p.is_file(), p.name.lower()))
|
|
28
28
|
except PermissionError:
|
|
29
29
|
return [f"{prefix}└── [permission denied]"]
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
if not show_hidden:
|
|
32
32
|
entries = [e for e in entries if not e.name.startswith(".")]
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
for i, entry in enumerate(entries):
|
|
35
35
|
is_last_entry = i == len(entries) - 1
|
|
36
36
|
connector = "└── " if is_last_entry else "├── "
|
|
37
37
|
lines.append(f"{prefix}{connector}{entry.name}")
|
|
38
|
-
|
|
38
|
+
|
|
39
39
|
if entry.is_dir():
|
|
40
40
|
extension = " " if is_last_entry else "│ "
|
|
41
41
|
sub_prefix = prefix + extension
|
|
42
42
|
lines.extend(
|
|
43
43
|
_build_tree_lines(
|
|
44
|
-
entry, sub_prefix, is_last_entry, show_hidden,
|
|
45
|
-
depth_limit, current_depth + 1
|
|
44
|
+
entry, sub_prefix, is_last_entry, show_hidden, depth_limit, current_depth + 1
|
|
46
45
|
)
|
|
47
46
|
)
|
|
48
|
-
|
|
47
|
+
|
|
49
48
|
return lines
|
|
50
49
|
|
|
51
50
|
|
|
52
|
-
def _build_tree_from_entries(entries: list) ->
|
|
51
|
+
def _build_tree_from_entries(entries: list) -> List[str]:
|
|
53
52
|
"""Build tree lines from commit tree entries (flat path/name/hash)."""
|
|
54
53
|
# Build nested dict: {dir: {subdir: {file: hash}}}
|
|
55
54
|
root: dict = {}
|
|
56
|
-
|
|
55
|
+
|
|
57
56
|
for path, name, hash_id in entries:
|
|
58
57
|
parts = (path.split("/") if path else []) + [name]
|
|
59
58
|
current = root
|
|
@@ -65,8 +64,8 @@ def _build_tree_from_entries(entries: list) -> list[str]:
|
|
|
65
64
|
if part not in current:
|
|
66
65
|
current[part] = {}
|
|
67
66
|
current = current[part]
|
|
68
|
-
|
|
69
|
-
def _render(node: dict, prefix: str = "") ->
|
|
67
|
+
|
|
68
|
+
def _render(node: dict, prefix: str = "") -> List[str]:
|
|
70
69
|
lines = []
|
|
71
70
|
# Directories first, then files; alphabetically within each
|
|
72
71
|
items = sorted(node.items(), key=lambda x: (not isinstance(x[1], dict), x[0].lower()))
|
|
@@ -80,16 +79,16 @@ def _build_tree_from_entries(entries: list) -> list[str]:
|
|
|
80
79
|
else:
|
|
81
80
|
lines.append(f"{prefix}{conn}{key} ({val[:8]})")
|
|
82
81
|
return lines
|
|
83
|
-
|
|
82
|
+
|
|
84
83
|
return _render(root)
|
|
85
84
|
|
|
86
85
|
|
|
87
86
|
class TreeCommand:
|
|
88
87
|
"""Show directory tree visually."""
|
|
89
|
-
|
|
88
|
+
|
|
90
89
|
name = "tree"
|
|
91
90
|
help = "Show working directory or commit tree visually"
|
|
92
|
-
|
|
91
|
+
|
|
93
92
|
@staticmethod
|
|
94
93
|
def add_arguments(parser: argparse.ArgumentParser):
|
|
95
94
|
parser.add_argument(
|
|
@@ -99,17 +98,19 @@ class TreeCommand:
|
|
|
99
98
|
help="Commit/branch to show (default: working directory)",
|
|
100
99
|
)
|
|
101
100
|
parser.add_argument(
|
|
102
|
-
"-a",
|
|
101
|
+
"-a",
|
|
102
|
+
"--all",
|
|
103
103
|
action="store_true",
|
|
104
104
|
help="Show hidden files",
|
|
105
105
|
)
|
|
106
106
|
parser.add_argument(
|
|
107
|
-
"-L",
|
|
107
|
+
"-L",
|
|
108
|
+
"--depth",
|
|
108
109
|
type=int,
|
|
109
110
|
default=None,
|
|
110
111
|
help="Limit depth of tree",
|
|
111
112
|
)
|
|
112
|
-
|
|
113
|
+
|
|
113
114
|
@staticmethod
|
|
114
115
|
def execute(args) -> int:
|
|
115
116
|
repo, code = require_repo()
|
|
@@ -122,19 +123,19 @@ class TreeCommand:
|
|
|
122
123
|
if not commit_hash:
|
|
123
124
|
print(f"Error: Unknown revision: {args.ref}")
|
|
124
125
|
return 1
|
|
125
|
-
|
|
126
|
+
|
|
126
127
|
commit = Commit.load(repo.object_store, commit_hash)
|
|
127
128
|
if not commit:
|
|
128
129
|
print(f"Error: Commit not found: {args.ref}")
|
|
129
130
|
return 1
|
|
130
|
-
|
|
131
|
+
|
|
131
132
|
tree = Tree.load(repo.object_store, commit.tree)
|
|
132
133
|
if not tree:
|
|
133
134
|
print(f"Error: Tree not found for {args.ref}")
|
|
134
135
|
return 1
|
|
135
|
-
|
|
136
|
+
|
|
136
137
|
entries = [(e.path, e.name, e.hash) for e in tree.entries]
|
|
137
|
-
|
|
138
|
+
|
|
138
139
|
print(f"📁 {args.ref} ({commit_hash[:8]})")
|
|
139
140
|
print("│")
|
|
140
141
|
for line in _build_tree_from_entries(entries):
|
|
@@ -145,12 +146,10 @@ class TreeCommand:
|
|
|
145
146
|
if not current_dir.exists():
|
|
146
147
|
print("Error: current/ directory not found.")
|
|
147
148
|
return 1
|
|
148
|
-
|
|
149
|
+
|
|
149
150
|
print(f"📁 current/ (working directory)")
|
|
150
151
|
print("│")
|
|
151
|
-
for line in _build_tree_lines(
|
|
152
|
-
current_dir, "", True, args.all, args.depth, 0
|
|
153
|
-
):
|
|
152
|
+
for line in _build_tree_lines(current_dir, "", True, args.all, args.depth, 0):
|
|
154
153
|
print(line)
|
|
155
|
-
|
|
154
|
+
|
|
156
155
|
return 0
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agmem verify - Belief consistency checker.
|
|
3
|
+
|
|
4
|
+
Scans semantic memories for logical contradictions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from ..commands.base import require_repo
|
|
11
|
+
from ..core.consistency import ConsistencyChecker, ConsistencyResult
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class VerifyCommand:
|
|
15
|
+
"""Verify belief consistency of semantic memories."""
|
|
16
|
+
|
|
17
|
+
name = "verify"
|
|
18
|
+
help = "Scan semantic memories for logical contradictions"
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def add_arguments(parser: argparse.ArgumentParser):
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--consistency",
|
|
24
|
+
"-c",
|
|
25
|
+
action="store_true",
|
|
26
|
+
default=True,
|
|
27
|
+
help="Check for contradictions (default)",
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--llm",
|
|
31
|
+
action="store_true",
|
|
32
|
+
help="Use LLM for triple extraction (requires OpenAI)",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def execute(args) -> int:
|
|
37
|
+
repo, code = require_repo()
|
|
38
|
+
if code != 0:
|
|
39
|
+
return code
|
|
40
|
+
|
|
41
|
+
checker = ConsistencyChecker(repo, llm_provider="openai" if args.llm else None)
|
|
42
|
+
result = checker.check(use_llm=args.llm)
|
|
43
|
+
|
|
44
|
+
print(f"Checked {result.files_checked} semantic file(s)")
|
|
45
|
+
if result.valid:
|
|
46
|
+
print("No contradictions found.")
|
|
47
|
+
return 0
|
|
48
|
+
|
|
49
|
+
print(f"\nFound {len(result.contradictions)} contradiction(s):")
|
|
50
|
+
for i, c in enumerate(result.contradictions, 1):
|
|
51
|
+
print(f"\n[{i}] {c.reason}")
|
|
52
|
+
print(
|
|
53
|
+
f" {c.triple1.source}:{c.triple1.line}: {c.triple1.subject} {c.triple1.predicate} {c.triple1.obj}"
|
|
54
|
+
)
|
|
55
|
+
print(
|
|
56
|
+
f" {c.triple2.source}:{c.triple2.line}: {c.triple2.subject} {c.triple2.predicate} {c.triple2.obj}"
|
|
57
|
+
)
|
|
58
|
+
print("\nUse 'agmem repair --strategy confidence' to attempt auto-fix.")
|
|
59
|
+
return 1
|