crumbs-cli 0.3.1__tar.gz → 0.3.2__tar.gz
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.
- {crumbs_cli-0.3.1 → crumbs_cli-0.3.2}/PKG-INFO +1 -1
- {crumbs_cli-0.3.1 → crumbs_cli-0.3.2}/crumbs/__init__.py +1 -1
- {crumbs_cli-0.3.1 → crumbs_cli-0.3.2}/crumbs/indexer.py +37 -0
- {crumbs_cli-0.3.1 → crumbs_cli-0.3.2}/crumbs/mcp.py +12 -1
- {crumbs_cli-0.3.1 → crumbs_cli-0.3.2}/crumbs_cli.egg-info/PKG-INFO +1 -1
- {crumbs_cli-0.3.1 → crumbs_cli-0.3.2}/pyproject.toml +1 -1
- {crumbs_cli-0.3.1 → crumbs_cli-0.3.2}/tests/test_crumbs.py +29 -0
- {crumbs_cli-0.3.1 → crumbs_cli-0.3.2}/LICENSE +0 -0
- {crumbs_cli-0.3.1 → crumbs_cli-0.3.2}/README.md +0 -0
- {crumbs_cli-0.3.1 → crumbs_cli-0.3.2}/crumbs/__main__.py +0 -0
- {crumbs_cli-0.3.1 → crumbs_cli-0.3.2}/crumbs/cli.py +0 -0
- {crumbs_cli-0.3.1 → crumbs_cli-0.3.2}/crumbs/digest.py +0 -0
- {crumbs_cli-0.3.1 → crumbs_cli-0.3.2}/crumbs/extractors.py +0 -0
- {crumbs_cli-0.3.1 → crumbs_cli-0.3.2}/crumbs/query.py +0 -0
- {crumbs_cli-0.3.1 → crumbs_cli-0.3.2}/crumbs/store.py +0 -0
- {crumbs_cli-0.3.1 → crumbs_cli-0.3.2}/crumbs_cli.egg-info/SOURCES.txt +0 -0
- {crumbs_cli-0.3.1 → crumbs_cli-0.3.2}/crumbs_cli.egg-info/dependency_links.txt +0 -0
- {crumbs_cli-0.3.1 → crumbs_cli-0.3.2}/crumbs_cli.egg-info/entry_points.txt +0 -0
- {crumbs_cli-0.3.1 → crumbs_cli-0.3.2}/crumbs_cli.egg-info/top_level.txt +0 -0
- {crumbs_cli-0.3.1 → crumbs_cli-0.3.2}/setup.cfg +0 -0
|
@@ -54,6 +54,43 @@ def _git_info(root: Path) -> Dict[str, str]:
|
|
|
54
54
|
return info
|
|
55
55
|
|
|
56
56
|
|
|
57
|
+
def newest_mtime(root: Path) -> float:
|
|
58
|
+
"""Most recent mtime among indexable files (cheap, stat-only walk).
|
|
59
|
+
|
|
60
|
+
Uses the same directory/file filters as :func:`index_repo` so the set of
|
|
61
|
+
files considered matches what would actually be indexed.
|
|
62
|
+
"""
|
|
63
|
+
newest = 0.0
|
|
64
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
65
|
+
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS and not d.startswith(".") or d in (".github",)]
|
|
66
|
+
for fn in filenames:
|
|
67
|
+
if fn in SKIP_FILES:
|
|
68
|
+
continue
|
|
69
|
+
try:
|
|
70
|
+
m = (Path(dirpath) / fn).stat().st_mtime
|
|
71
|
+
except OSError:
|
|
72
|
+
continue
|
|
73
|
+
if m > newest:
|
|
74
|
+
newest = m
|
|
75
|
+
return newest
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def is_stale(data: Dict[str, Any]) -> bool:
|
|
79
|
+
"""True if the repo on disk has changed since it was last indexed.
|
|
80
|
+
|
|
81
|
+
Returns ``True`` for unreadable/missing metadata so callers re-index;
|
|
82
|
+
returns ``False`` if the path no longer exists (nothing to refresh from).
|
|
83
|
+
"""
|
|
84
|
+
path = data.get("path")
|
|
85
|
+
indexed_at = data.get("indexed_at")
|
|
86
|
+
if not path or indexed_at is None:
|
|
87
|
+
return True
|
|
88
|
+
root = Path(path)
|
|
89
|
+
if not root.is_dir():
|
|
90
|
+
return False
|
|
91
|
+
return newest_mtime(root) > indexed_at
|
|
92
|
+
|
|
93
|
+
|
|
57
94
|
def index_repo(path: str, name: Optional[str] = None) -> Dict[str, Any]:
|
|
58
95
|
"""Index a repository at ``path`` and persist its crumbs.
|
|
59
96
|
|
|
@@ -75,9 +75,20 @@ def _tool_list(args: Dict[str, Any]) -> str:
|
|
|
75
75
|
|
|
76
76
|
|
|
77
77
|
def _resolve_or_index(selector: str) -> Optional[str]:
|
|
78
|
-
"""Resolve a repo selector
|
|
78
|
+
"""Resolve a repo selector, indexing (or re-indexing when stale) as needed.
|
|
79
|
+
|
|
80
|
+
An already-indexed repo is rebuilt if its source has changed since the last
|
|
81
|
+
index, so map/search/context never serve an out-of-date crumb map.
|
|
82
|
+
"""
|
|
79
83
|
rid = store.resolve(selector)
|
|
80
84
|
if rid:
|
|
85
|
+
data = store.load_repo(rid)
|
|
86
|
+
if data and not indexer.is_stale(data):
|
|
87
|
+
return rid
|
|
88
|
+
try:
|
|
89
|
+
indexer.index_repo(data["path"] if data else selector)
|
|
90
|
+
except (NotADirectoryError, FileNotFoundError, KeyError):
|
|
91
|
+
pass # keep the existing (possibly stale) index rather than failing
|
|
81
92
|
return rid
|
|
82
93
|
try:
|
|
83
94
|
indexer.index_repo(selector)
|
|
@@ -118,6 +118,35 @@ class TestIndexAndQuery(unittest.TestCase):
|
|
|
118
118
|
self.assertIn("demo", out)
|
|
119
119
|
|
|
120
120
|
|
|
121
|
+
class TestStaleness(unittest.TestCase):
|
|
122
|
+
def setUp(self):
|
|
123
|
+
self.repo = Path(tempfile.mkdtemp(prefix="stale-repo-"))
|
|
124
|
+
make_repo(self.repo)
|
|
125
|
+
self.data = indexer.index_repo(str(self.repo), name="stale")
|
|
126
|
+
|
|
127
|
+
def test_fresh_index_is_not_stale(self):
|
|
128
|
+
self.assertFalse(indexer.is_stale(self.data))
|
|
129
|
+
|
|
130
|
+
def test_new_file_marks_index_stale(self):
|
|
131
|
+
# Write a file whose mtime is newer than the recorded index time.
|
|
132
|
+
new_file = self.repo / "src" / "added.py"
|
|
133
|
+
new_file.write_text("def freshly_added():\n return 1\n")
|
|
134
|
+
os.utime(new_file, (self.data["indexed_at"] + 10, self.data["indexed_at"] + 10))
|
|
135
|
+
self.assertTrue(indexer.is_stale(self.data))
|
|
136
|
+
|
|
137
|
+
def test_map_auto_reindexes_when_stale(self):
|
|
138
|
+
new_file = self.repo / "src" / "later.py"
|
|
139
|
+
new_file.write_text("def added_later():\n return 2\n")
|
|
140
|
+
os.utime(new_file, (self.data["indexed_at"] + 10, self.data["indexed_at"] + 10))
|
|
141
|
+
# crumbs_map should detect staleness and pick up the new symbol.
|
|
142
|
+
out = mcp._tool_map({"repo": str(self.repo)})
|
|
143
|
+
self.assertIn("added_later", out)
|
|
144
|
+
|
|
145
|
+
def test_missing_path_is_not_stale(self):
|
|
146
|
+
ghost = dict(self.data, path="/nonexistent/path/xyz")
|
|
147
|
+
self.assertFalse(indexer.is_stale(ghost))
|
|
148
|
+
|
|
149
|
+
|
|
121
150
|
class TestMCP(unittest.TestCase):
|
|
122
151
|
@classmethod
|
|
123
152
|
def setUpClass(cls):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|