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