agmem 0.1.1__py3-none-any.whl → 0.1.3__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.3.dist-info}/METADATA +157 -16
- agmem-0.1.3.dist-info/RECORD +105 -0
- memvcs/__init__.py +1 -1
- memvcs/cli.py +45 -31
- memvcs/commands/__init__.py +9 -9
- memvcs/commands/add.py +83 -76
- memvcs/commands/audit.py +59 -0
- 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 +11 -1
- memvcs/commands/commit.py +40 -39
- memvcs/commands/daemon.py +109 -76
- memvcs/commands/decay.py +77 -0
- memvcs/commands/diff.py +56 -57
- memvcs/commands/distill.py +90 -0
- memvcs/commands/federated.py +53 -0
- memvcs/commands/fsck.py +86 -61
- memvcs/commands/garden.py +40 -35
- memvcs/commands/gc.py +51 -0
- memvcs/commands/graph.py +41 -48
- memvcs/commands/init.py +16 -24
- memvcs/commands/log.py +25 -40
- memvcs/commands/merge.py +69 -27
- memvcs/commands/pack.py +129 -0
- memvcs/commands/prove.py +66 -0
- memvcs/commands/pull.py +31 -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/resolve.py +130 -0
- 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 +110 -0
- memvcs/commands/when.py +115 -0
- memvcs/core/access_index.py +167 -0
- memvcs/core/audit.py +124 -0
- memvcs/core/config_loader.py +3 -1
- memvcs/core/consistency.py +214 -0
- memvcs/core/crypto_verify.py +280 -0
- memvcs/core/decay.py +185 -0
- memvcs/core/diff.py +158 -143
- memvcs/core/distiller.py +277 -0
- memvcs/core/encryption.py +169 -0
- memvcs/core/federated.py +86 -0
- memvcs/core/gardener.py +176 -145
- memvcs/core/hooks.py +48 -14
- memvcs/core/ipfs_remote.py +39 -0
- memvcs/core/knowledge_graph.py +135 -138
- memvcs/core/llm/__init__.py +10 -0
- memvcs/core/llm/anthropic_provider.py +50 -0
- memvcs/core/llm/base.py +27 -0
- memvcs/core/llm/factory.py +30 -0
- memvcs/core/llm/openai_provider.py +36 -0
- memvcs/core/merge.py +260 -170
- memvcs/core/objects.py +110 -101
- memvcs/core/pack.py +92 -0
- memvcs/core/pii_scanner.py +147 -146
- memvcs/core/privacy_budget.py +63 -0
- memvcs/core/refs.py +132 -115
- memvcs/core/remote.py +38 -0
- memvcs/core/repository.py +254 -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 +121 -0
- memvcs/core/test_runner.py +101 -93
- memvcs/core/trust.py +103 -0
- memvcs/core/vector_store.py +56 -36
- memvcs/core/zk_proofs.py +26 -0
- 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.3.dist-info}/WHEEL +0 -0
- {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/entry_points.txt +0 -0
- {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/top_level.txt +0 -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,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agmem verify - Belief consistency and cryptographic commit verification.
|
|
3
|
+
|
|
4
|
+
Scans semantic memories for logical contradictions; optionally verifies commit Merkle/signatures.
|
|
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 and/or cryptographic integrity of commits."""
|
|
16
|
+
|
|
17
|
+
name = "verify"
|
|
18
|
+
help = "Scan semantic memories for contradictions; optionally verify commit signatures"
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def add_arguments(parser: argparse.ArgumentParser):
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--consistency",
|
|
24
|
+
"-c",
|
|
25
|
+
action="store_true",
|
|
26
|
+
help="Check semantic memories for contradictions",
|
|
27
|
+
)
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"--crypto",
|
|
30
|
+
action="store_true",
|
|
31
|
+
help="Verify Merkle tree and signatures for commits",
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"--ref",
|
|
35
|
+
metavar="REF",
|
|
36
|
+
help="Commit or ref to verify (with --crypto); default HEAD",
|
|
37
|
+
)
|
|
38
|
+
parser.add_argument(
|
|
39
|
+
"--llm",
|
|
40
|
+
action="store_true",
|
|
41
|
+
help="Use LLM for triple extraction (requires OpenAI)",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def _run_crypto_verify(repo, ref: str = None) -> int:
|
|
46
|
+
"""Run cryptographic verification. Returns 0 if all OK, 1 on failure."""
|
|
47
|
+
from ..core.crypto_verify import verify_commit, load_public_key
|
|
48
|
+
|
|
49
|
+
if ref:
|
|
50
|
+
commit_hash = repo.resolve_ref(ref)
|
|
51
|
+
if not commit_hash:
|
|
52
|
+
print(f"Ref not found: {ref}")
|
|
53
|
+
return 1
|
|
54
|
+
else:
|
|
55
|
+
head = repo.refs.get_head()
|
|
56
|
+
if head["type"] == "branch":
|
|
57
|
+
commit_hash = repo.refs.get_branch_commit(head["value"])
|
|
58
|
+
else:
|
|
59
|
+
commit_hash = head.get("value")
|
|
60
|
+
if not commit_hash:
|
|
61
|
+
print("No commit to verify (empty repo).")
|
|
62
|
+
return 0
|
|
63
|
+
pub = load_public_key(repo.mem_dir)
|
|
64
|
+
ok, err = verify_commit(
|
|
65
|
+
repo.object_store, commit_hash, public_key_pem=pub, mem_dir=repo.mem_dir
|
|
66
|
+
)
|
|
67
|
+
if ok:
|
|
68
|
+
print(f"Commit {commit_hash[:8]} verified (Merkle + signature OK).")
|
|
69
|
+
return 0
|
|
70
|
+
print(f"Commit {commit_hash[:8]} verification failed: {err}")
|
|
71
|
+
return 1
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def execute(args) -> int:
|
|
75
|
+
repo, code = require_repo()
|
|
76
|
+
if code != 0:
|
|
77
|
+
return code
|
|
78
|
+
|
|
79
|
+
run_consistency = args.consistency
|
|
80
|
+
run_crypto = args.crypto
|
|
81
|
+
if not run_consistency and not run_crypto:
|
|
82
|
+
run_consistency = True
|
|
83
|
+
|
|
84
|
+
exit_code = 0
|
|
85
|
+
|
|
86
|
+
if run_crypto:
|
|
87
|
+
if VerifyCommand._run_crypto_verify(repo, args.ref) != 0:
|
|
88
|
+
exit_code = 1
|
|
89
|
+
|
|
90
|
+
if run_consistency:
|
|
91
|
+
checker = ConsistencyChecker(repo, llm_provider="openai" if args.llm else None)
|
|
92
|
+
result = checker.check(use_llm=args.llm)
|
|
93
|
+
|
|
94
|
+
print(f"Checked {result.files_checked} semantic file(s)")
|
|
95
|
+
if result.valid:
|
|
96
|
+
print("No contradictions found.")
|
|
97
|
+
else:
|
|
98
|
+
exit_code = 1
|
|
99
|
+
print(f"\nFound {len(result.contradictions)} contradiction(s):")
|
|
100
|
+
for i, c in enumerate(result.contradictions, 1):
|
|
101
|
+
print(f"\n[{i}] {c.reason}")
|
|
102
|
+
print(
|
|
103
|
+
f" {c.triple1.source}:{c.triple1.line}: {c.triple1.subject} {c.triple1.predicate} {c.triple1.obj}"
|
|
104
|
+
)
|
|
105
|
+
print(
|
|
106
|
+
f" {c.triple2.source}:{c.triple2.line}: {c.triple2.subject} {c.triple2.predicate} {c.triple2.obj}"
|
|
107
|
+
)
|
|
108
|
+
print("\nUse 'agmem repair --strategy confidence' to attempt auto-fix.")
|
|
109
|
+
|
|
110
|
+
return exit_code
|
memvcs/commands/when.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agmem when - Find when a specific fact was learned.
|
|
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 WhenCommand:
|
|
13
|
+
"""Find when a fact was learned in memory history."""
|
|
14
|
+
|
|
15
|
+
name = "when"
|
|
16
|
+
help = "Find when a specific fact was learned"
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def add_arguments(parser: argparse.ArgumentParser):
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"fact",
|
|
22
|
+
nargs="?",
|
|
23
|
+
help="Fact or text to search for (e.g., 'user prefers dark mode')",
|
|
24
|
+
)
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"--file",
|
|
27
|
+
"-f",
|
|
28
|
+
help="Limit search to specific file (e.g., semantic/preferences.md)",
|
|
29
|
+
)
|
|
30
|
+
parser.add_argument(
|
|
31
|
+
"--limit",
|
|
32
|
+
"-n",
|
|
33
|
+
type=int,
|
|
34
|
+
default=10,
|
|
35
|
+
help="Max commits to report (default: 10)",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def execute(args) -> int:
|
|
40
|
+
repo, code = require_repo()
|
|
41
|
+
if code != 0:
|
|
42
|
+
return code
|
|
43
|
+
|
|
44
|
+
if not args.fact:
|
|
45
|
+
print("Error: Fact to search for is required.")
|
|
46
|
+
print('Usage: agmem when "fact to find" [--file path]')
|
|
47
|
+
return 1
|
|
48
|
+
|
|
49
|
+
fact_lower = args.fact.lower()
|
|
50
|
+
file_filter = args.file.replace("current/", "").lstrip("/") if args.file else None
|
|
51
|
+
|
|
52
|
+
# Walk commit history from HEAD
|
|
53
|
+
head = repo.refs.get_head()
|
|
54
|
+
commit_hash = (
|
|
55
|
+
repo.refs.get_branch_commit(head["value"])
|
|
56
|
+
if head["type"] == "branch"
|
|
57
|
+
else head.get("value")
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
found = []
|
|
61
|
+
seen = set()
|
|
62
|
+
while commit_hash and len(found) < args.limit:
|
|
63
|
+
if commit_hash in seen:
|
|
64
|
+
break
|
|
65
|
+
seen.add(commit_hash)
|
|
66
|
+
|
|
67
|
+
commit = Commit.load(repo.object_store, commit_hash)
|
|
68
|
+
if not commit:
|
|
69
|
+
break
|
|
70
|
+
|
|
71
|
+
tree = repo.get_commit_tree(commit_hash)
|
|
72
|
+
if not tree:
|
|
73
|
+
commit_hash = commit.parents[0] if commit.parents else None
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
# Check each file in tree
|
|
77
|
+
for entry in tree.entries:
|
|
78
|
+
path = entry.path + "/" + entry.name if entry.path else entry.name
|
|
79
|
+
if file_filter and path != file_filter:
|
|
80
|
+
continue
|
|
81
|
+
if entry.obj_type != "blob":
|
|
82
|
+
continue
|
|
83
|
+
blob = Blob.load(repo.object_store, entry.hash)
|
|
84
|
+
if not blob:
|
|
85
|
+
continue
|
|
86
|
+
try:
|
|
87
|
+
content = blob.content.decode("utf-8", errors="replace")
|
|
88
|
+
except Exception:
|
|
89
|
+
continue
|
|
90
|
+
if fact_lower in content.lower():
|
|
91
|
+
found.append(
|
|
92
|
+
{
|
|
93
|
+
"commit": commit_hash,
|
|
94
|
+
"path": path,
|
|
95
|
+
"timestamp": commit.timestamp,
|
|
96
|
+
"author": commit.author,
|
|
97
|
+
"message": commit.message,
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
break # One match per commit
|
|
101
|
+
|
|
102
|
+
commit_hash = commit.parents[0] if commit.parents else None
|
|
103
|
+
|
|
104
|
+
if not found:
|
|
105
|
+
scope = f" in {file_filter}" if file_filter else ""
|
|
106
|
+
print(f'No commits found containing "{args.fact}"{scope}')
|
|
107
|
+
return 0
|
|
108
|
+
|
|
109
|
+
print(f'Fact "{args.fact}" found in {len(found)} commit(s):')
|
|
110
|
+
print()
|
|
111
|
+
for i, m in enumerate(found, 1):
|
|
112
|
+
print(f"[{i}] {m['commit'][:8]} {m['timestamp']} - {m['path']}")
|
|
113
|
+
print(f" {m['message'][:60]}")
|
|
114
|
+
print()
|
|
115
|
+
return 0
|