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.
Files changed (80) hide show
  1. {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/METADATA +20 -3
  2. agmem-0.1.2.dist-info/RECORD +86 -0
  3. memvcs/__init__.py +1 -1
  4. memvcs/cli.py +35 -31
  5. memvcs/commands/__init__.py +9 -9
  6. memvcs/commands/add.py +77 -76
  7. memvcs/commands/blame.py +46 -53
  8. memvcs/commands/branch.py +13 -33
  9. memvcs/commands/checkout.py +27 -32
  10. memvcs/commands/clean.py +18 -23
  11. memvcs/commands/clone.py +4 -1
  12. memvcs/commands/commit.py +40 -39
  13. memvcs/commands/daemon.py +81 -76
  14. memvcs/commands/decay.py +77 -0
  15. memvcs/commands/diff.py +56 -57
  16. memvcs/commands/distill.py +74 -0
  17. memvcs/commands/fsck.py +55 -61
  18. memvcs/commands/garden.py +28 -37
  19. memvcs/commands/graph.py +41 -48
  20. memvcs/commands/init.py +16 -24
  21. memvcs/commands/log.py +25 -40
  22. memvcs/commands/merge.py +16 -28
  23. memvcs/commands/pack.py +129 -0
  24. memvcs/commands/pull.py +4 -1
  25. memvcs/commands/push.py +4 -2
  26. memvcs/commands/recall.py +145 -0
  27. memvcs/commands/reflog.py +13 -22
  28. memvcs/commands/remote.py +1 -0
  29. memvcs/commands/repair.py +66 -0
  30. memvcs/commands/reset.py +23 -33
  31. memvcs/commands/resurrect.py +82 -0
  32. memvcs/commands/search.py +3 -4
  33. memvcs/commands/serve.py +2 -1
  34. memvcs/commands/show.py +66 -36
  35. memvcs/commands/stash.py +34 -34
  36. memvcs/commands/status.py +27 -35
  37. memvcs/commands/tag.py +23 -47
  38. memvcs/commands/test.py +30 -44
  39. memvcs/commands/timeline.py +111 -0
  40. memvcs/commands/tree.py +26 -27
  41. memvcs/commands/verify.py +59 -0
  42. memvcs/commands/when.py +115 -0
  43. memvcs/core/access_index.py +167 -0
  44. memvcs/core/config_loader.py +3 -1
  45. memvcs/core/consistency.py +214 -0
  46. memvcs/core/decay.py +185 -0
  47. memvcs/core/diff.py +158 -143
  48. memvcs/core/distiller.py +277 -0
  49. memvcs/core/gardener.py +164 -132
  50. memvcs/core/hooks.py +48 -14
  51. memvcs/core/knowledge_graph.py +134 -138
  52. memvcs/core/merge.py +248 -171
  53. memvcs/core/objects.py +95 -96
  54. memvcs/core/pii_scanner.py +147 -146
  55. memvcs/core/refs.py +132 -115
  56. memvcs/core/repository.py +174 -164
  57. memvcs/core/schema.py +155 -113
  58. memvcs/core/staging.py +60 -65
  59. memvcs/core/storage/__init__.py +20 -18
  60. memvcs/core/storage/base.py +74 -70
  61. memvcs/core/storage/gcs.py +70 -68
  62. memvcs/core/storage/local.py +42 -40
  63. memvcs/core/storage/s3.py +105 -110
  64. memvcs/core/temporal_index.py +112 -0
  65. memvcs/core/test_runner.py +101 -93
  66. memvcs/core/vector_store.py +41 -35
  67. memvcs/integrations/mcp_server.py +1 -3
  68. memvcs/integrations/web_ui/server.py +25 -26
  69. memvcs/retrieval/__init__.py +22 -0
  70. memvcs/retrieval/base.py +54 -0
  71. memvcs/retrieval/pack.py +128 -0
  72. memvcs/retrieval/recaller.py +105 -0
  73. memvcs/retrieval/strategies.py +314 -0
  74. memvcs/utils/__init__.py +3 -3
  75. memvcs/utils/helpers.py +52 -52
  76. agmem-0.1.1.dist-info/RECORD +0 -67
  77. {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/WHEEL +0 -0
  78. {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/entry_points.txt +0 -0
  79. {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/licenses/LICENSE +0 -0
  80. {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 = 'tag'
18
- help = 'Create, list, delete or verify a tag'
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
- 'name',
24
- nargs='?',
25
- help='Tag name'
26
- )
27
- parser.add_argument(
28
- 'commit',
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 'HEAD'
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 = 'test'
16
- help = 'Run memory regression tests to validate knowledge consistency'
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
- '--branch',
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
- '--init',
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
- '-v', '--verbose',
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
- vector_store = VectorStore(repo.root / '.mem')
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 / 'tests'
104
+ tests_dir = repo.root / "tests"
119
105
  tests_dir.mkdir(exist_ok=True)
120
-
121
- template_file = tests_dir / 'example_tests.yaml'
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
- ) -> list[str]:
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) -> list[str]:
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 = "") -> list[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", "--all",
101
+ "-a",
102
+ "--all",
103
103
  action="store_true",
104
104
  help="Show hidden files",
105
105
  )
106
106
  parser.add_argument(
107
- "-L", "--depth",
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