gabriel / muse public
index_rebuild.py python
666 lines 25.7 KB
Raw
sha256:6ed6b11aceb96bef850842a7aaac54c04843fa9fba5d1200e4bc3845e12d4142 docs: add muse-tag-guide mist (complete idiomatic guide wit… Sonnet 4.6 19 days ago
1 """muse code index — manage and rebuild the optional local index layer.
2
3 Indexes live under ``.muse/indices/`` and are fully derived from the commit
4 history. They are optional — all commands work without them, but indexes
5 dramatically accelerate repeated queries on large repositories.
6
7 Available indexes
8 -----------------
9
10 ``symbol_history``
11 Maps every symbol address to its full event timeline across all commits.
12 Reduces ``muse code symbol-log``, ``muse code lineage``, and
13 ``muse code query-history`` from O(commits × files) to O(1) lookups.
14
15 ``hash_occurrence``
16 Maps every ``body_hash`` to the list of addresses that share it.
17 Reduces ``muse code clones`` and ``muse code find-symbol hash=`` to O(1).
18
19 Sub-commands
20 ------------
21
22 ``muse code index status``
23 Show the status, entry count, and last-updated time of each index.
24
25 ``muse code index rebuild [--index NAME] [--dry-run]``
26 Rebuild one or all indexes by walking the entire commit history.
27 Safe to run multiple times. Pass ``--dry-run`` to see what would be
28 built without writing anything.
29
30 ``muse code index purge [--index NAME]``
31 Delete one or all local index files. The next rebuild recreates them.
32
33 Usage::
34
35 muse code index status
36 muse code index status --json
37 muse code index rebuild
38 muse code index rebuild --json
39 muse code index rebuild --index symbol_history
40 muse code index rebuild --index hash_occurrence
41 muse code index rebuild --dry-run
42 muse code index purge
43 muse code index purge --index symbol_history
44
45 JSON output — ``muse code index status --json``::
46
47 {"indexes": [
48 {"name": "symbol_history", "status": "present", "entries": 1024,
49 "updated_at": "2026-03-21T12:00:00+00:00"},
50 {"name": "hash_occurrence", "status": "absent", "entries": 0,
51 "updated_at": null}
52 ],
53 "exit_code": 0,
54 "duration_ms": 12.5}
55
56 JSON output — ``muse code index rebuild --json``::
57
58 {"schema_version": "0.1.5",
59 "rebuilt": ["symbol_history", "hash_occurrence"],
60 "symbol_history_addresses": 512, "symbol_history_events": 2048,
61 "hash_occurrence_clusters": 31, "hash_occurrence_addresses": 87,
62 "exit_code": 0, "duration_ms": 8432.1}
63 """
64
65 import argparse
66 import json
67 import logging
68 import pathlib
69 from typing import TypedDict
70
71 from muse.core.types import short_id
72 from muse.core.paths import muse_dir as _muse_dir, ref_path as _ref_path
73 from muse.core.envelope import EnvelopeJson, make_envelope
74 from muse.core.errors import ExitCode
75 from muse.core.timing import start_timer
76 from muse.core.indices import (
77 KNOWN_INDEX_NAMES,
78 HashOccurrenceIndex,
79 IndexInfoEntry,
80 SymbolHistoryEntry,
81 SymbolHistoryIndex,
82 index_info,
83 purge_index,
84 save_hash_occurrence,
85 save_symbol_history,
86 )
87 from muse.core.object_store import read_object
88 from muse.core.refs import read_ref
89 from muse.core.repo import require_repo
90 from muse.core.refs import read_current_branch
91 from muse.core.commits import get_all_commits
92 from muse.core.snapshots import get_commit_snapshot_manifest
93 from muse.core.symbol_cache import SymbolCache, load_symbol_cache
94 from muse.plugins.code._query import is_semantic
95 from muse.plugins.code.ast_parser import parse_symbols
96 from muse.core.validation import sanitize_display
97
98 type _BlobCache = dict[str, bytes]
99 type _FileManifest = dict[str, str]
100 type _ManifestCache = dict[str, _FileManifest]
101 logger = logging.getLogger(__name__)
102
103 # ---------------------------------------------------------------------------
104 # TypedDicts for JSON output envelopes
105 # ---------------------------------------------------------------------------
106
107 class _RebuildResult(EnvelopeJson, total=False):
108 """JSON envelope for ``muse code index rebuild --json``.
109
110 Fields
111 ------
112 dry_run True when --dry-run was passed; no files written.
113 rebuilt Index names that were (or would be) rebuilt.
114 symbol_history_addresses Distinct symbol addresses in the new index.
115 Present only when symbol_history was rebuilt.
116 symbol_history_events Total insert/delete/replace events across all addresses.
117 Present only when symbol_history was rebuilt.
118 hash_occurrence_clusters Hash clusters with more than one address.
119 Present only when hash_occurrence was rebuilt.
120 hash_occurrence_addresses Total address count across all clone clusters.
121 Present only when hash_occurrence was rebuilt.
122 """
123
124 dry_run: bool
125 rebuilt: list[str]
126 symbol_history_addresses: int
127 symbol_history_events: int
128 hash_occurrence_clusters: int
129 hash_occurrence_addresses: int
130
131 class _StatusIndexEntry(TypedDict):
132 """One index entry in the status JSON output."""
133
134 name: str
135 status: str
136 entries: int
137 updated_at: str | None
138
139 class _StatusResult(EnvelopeJson):
140 """JSON envelope for ``muse code index status --json``.
141
142 Fields
143 ------
144 indexes List of index status entries (name, status, entries, updated_at).
145 """
146
147 indexes: list[_StatusIndexEntry]
148
149 class _PurgeResult(EnvelopeJson):
150 """JSON envelope for ``muse code index purge --json``.
151
152 Fields
153 ------
154 purged Index names whose files were found and deleted.
155 skipped Index names that were not present (nothing to delete).
156 """
157
158 purged: list[str]
159 skipped: list[str]
160
161 # ---------------------------------------------------------------------------
162 # Index build logic
163 # ---------------------------------------------------------------------------
164
165 def _build_symbol_history(
166 root: pathlib.Path,
167 symbol_cache: SymbolCache | None = None,
168 ) -> SymbolHistoryIndex:
169 """Walk all commits oldest-first and build the symbol history index.
170
171 Performance notes
172 -----------------
173 * A ``manifest_cache`` (keyed by commit_id) ensures that each snapshot
174 manifest is fetched at most once per rebuild, regardless of how many
175 symbol ops share the same commit.
176
177 * A ``blob_cache`` (keyed by obj_id) ensures that each source blob is
178 ``read_object``'d at most once per rebuild.
179
180 * ``symbol_cache`` is the **persistent** cross-run parse cache
181 (:class:`~muse.core.symbol_cache.SymbolCache`). Because obj_id is the
182 SHA-256 of file bytes (content-addressed), a file that did not change
183 between two commits always produces a cache hit — its AST is never
184 re-parsed. On a warm cache a 300-commit rebuild drops from minutes to
185 seconds.
186
187 Together these three layers reduce I/O from
188 O(commits × ops × files) on the first run to
189 O(1) per unique (obj_id, address) pair on subsequent runs.
190 """
191 all_commits = sorted(
192 get_all_commits(root),
193 key=lambda c: c.committed_at,
194 )
195 index: SymbolHistoryIndex = {}
196
197 # manifest_cache: commit_id → {file_path: obj_id}
198 manifest_cache: _ManifestCache = {}
199 # blob_cache: obj_id → raw bytes (within this run; SymbolCache handles cross-run)
200 blob_cache: _BlobCache = {}
201
202 for commit in all_commits:
203 if commit.structured_delta is None:
204 continue
205 committed_at = commit.committed_at.isoformat()
206 ops = commit.structured_delta.get("ops", [])
207
208 # Fetch the manifest once per commit, not once per child op.
209 if commit.commit_id not in manifest_cache:
210 raw_manifest = get_commit_snapshot_manifest(root, commit.commit_id)
211 if raw_manifest is None:
212 logger.debug(
213 "Missing snapshot manifest for commit %s — skipping",
214 short_id(commit.commit_id),
215 )
216 continue
217 manifest_cache[commit.commit_id] = raw_manifest
218 manifest = manifest_cache[commit.commit_id]
219
220 for op in ops:
221 if op["op"] != "patch":
222 continue
223 for child in op.get("child_ops", []):
224 addr = child["address"]
225 if "::" not in addr:
226 continue
227 file_path = addr.split("::")[0]
228 if not is_semantic(file_path):
229 continue
230 child_op = child["op"]
231 if child_op not in ("insert", "delete", "replace"):
232 continue
233
234 # Extract hash fields from the snapshot blob.
235 # Resolution order:
236 # 1. symbol_cache (persistent JSON cache, keyed by obj_id)
237 # 2. blob_cache (in-run bytes cache, avoids duplicate reads)
238 # 3. read_object + parse_symbols (cache miss — first encounter)
239 obj_id = manifest.get(file_path)
240 body_hash = ""
241 signature_id = ""
242 content_id = ""
243 if obj_id:
244 # Try the persistent SymbolCache first.
245 tree = symbol_cache.get(obj_id) if symbol_cache else None
246 if tree is None:
247 # Fetch bytes (in-run blob_cache avoids duplicate reads).
248 if obj_id not in blob_cache:
249 raw = read_object(root, obj_id)
250 if raw is not None:
251 blob_cache[obj_id] = raw
252 blob = blob_cache.get(obj_id)
253 if blob is not None:
254 tree = parse_symbols(blob, file_path)
255 if symbol_cache is not None:
256 symbol_cache.put(obj_id, tree)
257 if tree is not None:
258 rec = tree.get(addr)
259 if rec:
260 body_hash = rec["body_hash"]
261 signature_id = rec["signature_id"]
262 content_id = rec["content_id"]
263
264 if not content_id:
265 # Fall back to the content_id stored in the delta itself.
266 if child_op == "insert":
267 content_id = str(child.get("content_id") or "")
268 elif child_op == "delete":
269 content_id = str(child.get("content_id") or "")
270 elif child_op == "replace":
271 content_id = str(child.get("new_content_id") or "")
272
273 entry = SymbolHistoryEntry(
274 commit_id=commit.commit_id,
275 committed_at=committed_at,
276 op=child_op,
277 content_id=content_id,
278 body_hash=body_hash,
279 signature_id=signature_id,
280 )
281 index.setdefault(addr, []).append(entry)
282
283 return index
284
285 def _build_hash_occurrence(root: pathlib.Path) -> HashOccurrenceIndex:
286 """Walk the HEAD snapshot and build the hash occurrence index."""
287 try:
288 muse_dir = _muse_dir(root)
289 branch = read_current_branch(root)
290 head_commit_id = read_ref(_ref_path(root, branch)) or ""
291 except OSError as exc:
292 logger.debug("Could not determine HEAD commit for hash_occurrence build: %s", exc)
293 return {}
294
295 if not head_commit_id:
296 return {}
297
298 raw_manifest = get_commit_snapshot_manifest(root, head_commit_id)
299 if raw_manifest is None:
300 logger.debug(
301 "Missing snapshot manifest for HEAD %s — hash_occurrence will be empty",
302 short_id(head_commit_id),
303 )
304 return {}
305
306 index: HashOccurrenceIndex = {}
307 for file_path, obj_id in sorted(raw_manifest.items()):
308 if not is_semantic(file_path):
309 continue
310 raw = read_object(root, obj_id)
311 if raw is None:
312 continue
313 tree = parse_symbols(raw, file_path)
314 for addr, rec in tree.items():
315 if rec["kind"] == "import":
316 continue
317 bh = rec["body_hash"]
318 index.setdefault(bh, []).append(addr)
319
320 # Remove trivial (size-1) entries — they are not clones.
321 return {h: addrs for h, addrs in index.items() if len(addrs) > 1}
322
323 # ---------------------------------------------------------------------------
324 # Sub-commands
325 # ---------------------------------------------------------------------------
326
327 def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
328 """Register the index subcommand."""
329 parser = subparsers.add_parser(
330 "index",
331 help="Manage the optional local index layer.",
332 description=__doc__,
333 formatter_class=argparse.RawDescriptionHelpFormatter,
334 )
335 subs = parser.add_subparsers(dest="subcommand", metavar="SUBCOMMAND")
336 subs.required = True
337
338 # --- purge ---
339 purge_p = subs.add_parser(
340 "purge",
341 help="Delete one or all local index files.",
342 description=(
343 "Delete index files under .muse/indices/. The canonical commit\n"
344 "history and object store are never touched — indexes are fully\n"
345 "rebuildable at any time with 'muse code index rebuild'.\n"
346 "Absent indexes are silently skipped (exit 0).\n\n"
347 "Agent quickstart\n"
348 "----------------\n"
349 " muse code index purge --json\n"
350 " muse code index purge -j\n"
351 " muse code index purge -j | jq .purged\n"
352 " muse code index purge --index symbol_history -j\n\n"
353 "JSON output schema\n"
354 "------------------\n"
355 ' {"schema_version": "<str>",\n'
356 ' "purged": ["symbol_history", ...],\n'
357 ' "skipped": ["hash_occurrence", ...]}\n\n'
358 "Exit codes\n"
359 "----------\n"
360 " 0 — operation completed (skipped indexes do not cause failure)\n"
361 " 2 — not inside a Muse repository\n"
362 ),
363 formatter_class=argparse.RawDescriptionHelpFormatter,
364 )
365 purge_p.add_argument(
366 "--index", "-i",
367 dest="index_name",
368 default=None,
369 metavar="NAME",
370 choices=list(KNOWN_INDEX_NAMES),
371 help="Purge a specific index. Default: purge all.",
372 )
373 purge_p.add_argument("--json", "-j", dest="json_out", action="store_true",
374 help="Emit purge summary as JSON.")
375 purge_p.set_defaults(func=run_purge)
376
377 # --- rebuild ---
378 rebuild_p = subs.add_parser(
379 "rebuild",
380 help="Rebuild local indexes from the full commit history.",
381 description=(
382 "Rebuild symbol_history and/or hash_occurrence under .muse/indices/.\n"
383 "Safe to run any number of times — atomic writes prevent corruption.\n"
384 "A warm SymbolCache means unchanged files are never re-parsed.\n\n"
385 "Use --dry-run to compute statistics without writing to disk.\n\n"
386 "Agent quickstart\n"
387 "----------------\n"
388 " muse code index rebuild --json\n"
389 " muse code index rebuild -j\n"
390 " muse code index rebuild -j | jq .rebuilt\n"
391 " muse code index rebuild --index symbol_history -j\n"
392 " muse code index rebuild --dry-run -j\n\n"
393 "JSON output schema\n"
394 "------------------\n"
395 ' {"schema_version": "<str>", "dry_run": <bool>,\n'
396 ' "rebuilt": ["symbol_history", "hash_occurrence"],\n'
397 ' "symbol_history_addresses": <int>, "symbol_history_events": <int>,\n'
398 ' "hash_occurrence_clusters": <int>, "hash_occurrence_addresses": <int>}\n\n'
399 " Note: *_addresses / *_events / *_clusters keys are absent when the\n"
400 " corresponding index was not rebuilt.\n\n"
401 "Exit codes\n"
402 "----------\n"
403 " 0 — rebuild (or dry run) completed successfully\n"
404 " 2 — not inside a Muse repository\n"
405 ),
406 formatter_class=argparse.RawDescriptionHelpFormatter,
407 )
408 rebuild_p.add_argument(
409 "--index", "-i",
410 dest="index_name",
411 default=None,
412 metavar="NAME",
413 choices=list(KNOWN_INDEX_NAMES),
414 help="Rebuild a specific index. Default: rebuild all.",
415 )
416 rebuild_p.add_argument("--dry-run", "-n", action="store_true",
417 help="Compute what would be built without writing anything.")
418 rebuild_p.add_argument("--verbose", "-v", action="store_true", help="Show progress.")
419 rebuild_p.add_argument("--json", "-j", dest="json_out", action="store_true",
420 help="Emit rebuild summary as JSON.")
421 rebuild_p.set_defaults(func=run_rebuild)
422
423 # --- status ---
424 status_p = subs.add_parser(
425 "status",
426 help="Show the status and entry count of each local index.",
427 description=(
428 "Report the on-disk state of every index under .muse/indices/.\n"
429 "Each index is either present (valid), absent (not yet built),\n"
430 "or corrupt (exists but failed to parse).\n\n"
431 "Agent quickstart\n"
432 "----------------\n"
433 " muse code index status --json\n"
434 " muse code index status -j\n"
435 " muse code index status -j | jq '.[].status'\n"
436 " muse code index status -j | jq '.[] | select(.status != \"present\")'\n\n"
437 "JSON output schema\n"
438 "------------------\n"
439 " [{\"name\": \"symbol_history\", \"status\": \"present|absent|corrupt\",\n"
440 ' "entries": <int>, "updated_at": "<iso8601>|null"}, ...]\n\n'
441 "Exit codes\n"
442 "----------\n"
443 " 0 — status emitted (absent/corrupt indexes do NOT cause non-zero exit)\n"
444 " 2 — not inside a Muse repository\n"
445 ),
446 formatter_class=argparse.RawDescriptionHelpFormatter,
447 )
448 status_p.add_argument("--json", "-j", dest="json_out", action="store_true", help="Emit index status as JSON.")
449 status_p.set_defaults(func=run_status)
450
451 def run_status(args: argparse.Namespace) -> None:
452 """Show the status and entry count of each local Muse index.
453
454 Each index is reported as ``present`` (valid), ``absent`` (not built yet),
455 or ``corrupt`` (file exists but failed to parse). Absent or corrupt indexes
456 do not cause a non-zero exit — use this to decide whether to run ``rebuild``.
457
458 Agent quickstart::
459
460 muse code index status --json
461
462 JSON fields::
463
464 indexes List of {name, status, entries, updated_at} per index.
465 muse_version Muse release that produced this output.
466 schema Envelope schema version (int).
467 exit_code Always 0.
468 duration_ms Wall-clock milliseconds for the status check.
469 timestamp ISO-8601 UTC timestamp of command completion.
470 warnings List of non-fatal advisory messages.
471
472 Exit codes::
473
474 0 Status emitted (even if indexes are absent or corrupt).
475 """
476 elapsed = start_timer()
477 json_out: bool = args.json_out
478
479 root = require_repo()
480 infos: list[IndexInfoEntry] = index_info(root)
481
482 if json_out:
483 indexes: list[_StatusIndexEntry] = [
484 _StatusIndexEntry(
485 name=info["name"],
486 status=info["status"],
487 entries=info["entries"],
488 updated_at=info["updated_at"],
489 )
490 for info in infos
491 ]
492 print(json.dumps(_StatusResult(
493 **make_envelope(elapsed),
494 indexes=indexes,
495 )))
496 return
497
498 print("\nLocal index status:")
499 print("─" * 50)
500 for info in infos:
501 status = info["status"]
502 name = info["name"]
503 updated = (info["updated_at"] or "")[:19]
504 entries = info["entries"]
505 if status == "present":
506 print(f" ✅ {sanitize_display(name):<20} {entries:>8} entries (updated {updated})")
507 elif status == "absent":
508 print(f" ⬜ {sanitize_display(name):<20} (not built — run: muse code index rebuild)")
509 else:
510 print(f" ❌ {sanitize_display(name):<20} corrupt — run: muse code index rebuild")
511 print()
512
513 def run_rebuild(args: argparse.Namespace) -> None:
514 """Rebuild local indexes from the full commit history.
515
516 Walks all commits oldest-first to build ``symbol_history`` and/or
517 ``hash_occurrence`` under ``.muse/indices/``. A shared SymbolCache means
518 AST parses are never repeated for unchanged files — on a warm cache a
519 300-commit rebuild drops from minutes to seconds. With ``--dry-run``,
520 the build runs in memory and statistics are reported without writing anything.
521
522 Agent quickstart::
523
524 muse code index rebuild --json
525 muse code index rebuild --index symbol_history --json
526 muse code index rebuild --dry-run --json
527
528 JSON fields::
529
530 dry_run true when --dry-run was passed; no files written.
531 rebuilt List of index names rebuilt.
532 symbol_history_addresses Distinct symbol addresses (symbol_history only).
533 symbol_history_events Total insert/delete/replace events (symbol_history only).
534 hash_occurrence_clusters Clone clusters (hash_occurrence only).
535 hash_occurrence_addresses Total addresses across clone clusters (hash_occurrence only).
536 muse_version Muse release that produced this output.
537 schema Envelope schema version (int).
538 exit_code Always 0.
539 duration_ms Wall-clock milliseconds for the rebuild.
540 timestamp ISO-8601 UTC timestamp of command completion.
541 warnings List of non-fatal advisory messages.
542
543 Exit codes::
544
545 0 Rebuild completed (or dry run computed) successfully.
546 """
547 elapsed = start_timer()
548 index_name: str | None = args.index_name
549 dry_run: bool = args.dry_run
550 verbose: bool = args.verbose
551 json_out: bool = args.json_out
552
553 root = require_repo()
554
555 build_all = index_name is None
556 built: list[str] = []
557 result: _RebuildResult = {
558 "dry_run": dry_run,
559 }
560
561 # Load the persistent SymbolCache once — it is shared across both index
562 # builds and saved at the end. This gives cross-run caching of AST parses
563 # so that unchanged files are never re-parsed.
564 sym_cache = load_symbol_cache(root)
565
566 if build_all or index_name == "symbol_history":
567 if verbose and not json_out:
568 print("Building symbol_history index…")
569 idx = _build_symbol_history(root, symbol_cache=sym_cache)
570 if not dry_run:
571 save_symbol_history(root, idx)
572 n_events = sum(len(evts) for evts in idx.values())
573 result["symbol_history_addresses"] = len(idx)
574 result["symbol_history_events"] = n_events
575 if not json_out:
576 tag = " (dry run)" if dry_run else ""
577 print(f" ✅ symbol_history — {len(idx)} addresses, {n_events} events{tag}")
578 built.append("symbol_history")
579
580 if build_all or index_name == "hash_occurrence":
581 if verbose and not json_out:
582 print("Building hash_occurrence index…")
583 idx2 = _build_hash_occurrence(root)
584 if not dry_run:
585 save_hash_occurrence(root, idx2)
586 n_clones = sum(len(addrs) for addrs in idx2.values())
587 result["hash_occurrence_clusters"] = len(idx2)
588 result["hash_occurrence_addresses"] = n_clones
589 if not json_out:
590 tag = " (dry run)" if dry_run else ""
591 print(f" ✅ hash_occurrence — {len(idx2)} clone clusters, {n_clones} addresses{tag}")
592 built.append("hash_occurrence")
593
594 result["rebuilt"] = built
595
596 # Persist any newly cached parse results so the next rebuild is faster.
597 if not dry_run:
598 sym_cache.save()
599
600 if json_out:
601 print(json.dumps({**make_envelope(elapsed), **result}))
602 return
603
604 action = "Computed" if dry_run else "Rebuilt"
605 suffix = " — no files written (dry run)" if dry_run else " under .muse/indices/"
606 print(f"\n{action} {len(built)} index(es){suffix}")
607 if not dry_run:
608 print("Run 'muse code index status' to verify.")
609
610 def run_purge(args: argparse.Namespace) -> None:
611 """Delete one or all local Muse index files under ``.muse/indices/``.
612
613 Indexes are derived data — the commit history and object store are never
614 touched. Missing indexes are skipped and reported under ``skipped``.
615 Any purged index can be recreated with ``muse code index rebuild``.
616
617 Agent quickstart::
618
619 muse code index purge --json
620 muse code index purge --index symbol_history --json
621
622 JSON fields::
623
624 purged Index names whose files were deleted.
625 skipped Index names that were not present (nothing to delete).
626 muse_version Muse release that produced this output.
627 schema Envelope schema version (int).
628 exit_code Always 0.
629 duration_ms Wall-clock milliseconds for the purge.
630 timestamp ISO-8601 UTC timestamp of command completion.
631 warnings List of non-fatal advisory messages.
632
633 Exit codes::
634
635 0 Operation completed (skipped indexes do not cause non-zero exit).
636 """
637 elapsed = start_timer()
638 index_name: str | None = args.index_name
639 json_out: bool = args.json_out
640
641 root = require_repo()
642 muse_dir = _muse_dir(root)
643 names = list(KNOWN_INDEX_NAMES) if index_name is None else [index_name]
644
645 purged: list[str] = []
646 skipped: list[str] = []
647 for name in names:
648 if purge_index(root, name):
649 purged.append(name)
650 else:
651 skipped.append(name)
652
653 if json_out:
654 print(json.dumps(_PurgeResult(
655 **make_envelope(elapsed),
656 purged=purged,
657 skipped=skipped,
658 )))
659 return
660
661 for name in purged:
662 print(f" 🗑️ {name} — deleted")
663 for name in skipped:
664 print(f" ⬜ {name} — not present, nothing to delete")
665 if purged:
666 print(f"\nPurged {len(purged)} index(es). Run 'muse code index rebuild' to recreate.")
File History 1 commit
sha256:6ed6b11aceb96bef850842a7aaac54c04843fa9fba5d1200e4bc3845e12d4142 docs: add muse-tag-guide mist (complete idiomatic guide wit… Sonnet 4.6 19 days ago