# `muse symlog` — Follow-up Items ## Background Issue #51 delivered the full symlog foundation: per-symbol journaling at commit time, `muse symlog` read CLI with filters, lifecycle management (expire/delete/GC), and `@{N}` ref resolution for `muse code cat`, `muse symlog resolve`, and `muse symlog diff`. All 55 test IDs (SL_01–SL_55) are green. This issue captures the items explicitly deferred from #51 plus useful extensions that emerged during implementation. --- ## Deferred from #51 ### 1. Analytics commands — `muse symlog coupling` and `muse symlog hotspots` ✅ IMPLEMENTED The live symlog journal is a rich, already-written dataset. Two analytics commands can read it in O(1) per symbol without touching commit history: **`muse symlog hotspots [--top N] [--file path] [--json]`** — ranks symbols by churn (number of `symbol-modified` entries in the journal). Complements `muse code hotspots` (which reads commit history) with a zero-scan alternative that is always up to date. **`muse symlog coupling [--file path] [--json]`** — finds symbol pairs that share commit IDs across their logs (i.e. they always change together). Surfaced purely from symlog data, no AST traversal needed. ### 2. `muse blame` integration ✅ IMPLEMENTED `muse blame "file.py::Symbol"` currently reconstructs authorship by scanning commit history. With symlogs in place, the per-symbol change record already contains author and commit ID at each index. `blame` should prefer the symlog when available — O(1) per symbol instead of O(commits). **Note on command path**: the actual command is `muse code blame ADDRESS` (same naming situation as item 7). It now prefers the symlog journal when one exists, falling back to the full BFS commit-history scan only when: no journal exists yet, `--from REF` is given (the journal has no concept of "as reachable from an older ref" the way the BFS walk's starting boundary does), or `--kind moved`/`--kind signature` is requested (categories the journal's created/modified/deleted/renamed classification can never produce). Rename chains are followed via the journal's own `born_from` pointers, matching the pre-existing rename-tracking behavior. ### 3. `muse code narrative` rewrite ✅ IMPLEMENTED `muse code narrative "file.py::Symbol"` generates a plain-English description of a symbol's lifecycle. It currently works from AST diffs across commits. The symlog journal is the canonical source of that history now — narrative should read from symlog entries, gaining richer metadata (operation, author, born-from chains). **Decision**: not a literal "read narrative data from symlog entries" swap — the journal's created/modified/deleted/renamed classification is coarser than the impl-vs-signature distinction this command's storytelling depends on (separate "body rewrite"/"signature change" counts, breaking-change detection via SemVer bump, detailed `old_summary`/`new_summary` text), none of which the journal carries. Instead, the journal is used as an INDEX narrowing down which commits actually touched the symbol, replacing a full commit-DAG BFS walk — each relevant commit's real `structured_delta` is still read for full fidelity. `--since` has no equivalent against the flat journal, so it forces the original full-history fallback, same gating as `muse code blame`'s `--from` (item 2). This incidentally **fixes a real latent bug**, not just a performance win: the original BFS walk tracked one fixed symbol address for the entire scan, so a renamed symbol's history silently stopped at the rename boundary (the rename op is recorded under the OLD address, which a fixed-address scan of the NEW address never matches). The journal's `born_from` chain crosses that boundary natively via the same mechanism `muse symlog --follow` already uses, so a renamed symbol's narrative now includes its pre-rename history for the first time — confirmed live: a two-commit rename case went from `commits_analysed: 1` (missing the creation event) to `commits_analysed: 2` (creation event correctly surfaced under the old address). ### 4. Remote symlog sharing ✅ IMPLEMENTED Symlogs are currently local-only — they are not pushed or pulled. For teams sharing a MuseHub remote, symlogs are silently missing on clones. The correct design: - `muse push` bundles `.muse/symlogs/` alongside commits. - `muse pull` merges incoming symlog entries (append-only, no conflicts). - `muse bundle create` includes symlogs in the msgpack payload. **Implementation**: mirrors muse#54 item 4's reflog remote-sharing precedent (same wire-conversion / idempotent-merge / transport-method / server-table shape), with one structural difference: symlog is per-symbol, not per-branch, so there is no "local vs remote-tracking namespace" split — reflog needs that split to avoid conflating a remote's ref-movement history with purely local activity, but a symbol's commit history is identical for every clone, so entries merge directly into the same local journal a `muse commit` would write to. - `muse push --symlog` (or `push.symlog=true` config) batches every local symbol's journal entries into one `POST {url}/symlog` request (`{"symbols": {addr: [entries...]}}`). - `muse pull` always best-effort fetches via `GET {url}/symlog` and merges directly into local journals. - `muse bundle create`/`unbundle` carry every local symlog entry unconditionally (bundles are meant to be a comprehensive, self-contained offline transfer, so no `--symlog`-style opt-in is needed there). - Server side (MuseHub): new `musehub_symlog_entries` table (migration 0081), keyed by a content-hash `entry_id` (includes `symbol_addr` in the hash, unlike reflog's per-branch hash) for the same `ON CONFLICT DO NOTHING` append-only semantics, plus `wire_push_symlog`/`wire_get_symlog` routes. ### 5. Symlog entries for non-code domains — ⏸️ CONFIRMED STILL BLOCKED The extractor returns `{}` for non-parseable files, so MIDI tracks, binary assets, and schema files produce no symlog entries. For music repos this is the wrong trade-off: a MIDI symbol (`track::chorus`) should produce a symlog entry when the Harmony domain plugin identifies a change. Deferred until the domain plugin API supports content-ID extraction for non-code artifacts. **Re-verified while clearing this backlog** (not just left alone on faith): `extract_symbols()`/`_extract_body_hashes()` in `muse/core/symlog.py` import `muse.plugins.code.ast_parser.parse_symbols` directly and unconditionally — there is no `MuseDomainPlugin.extract_symbols()`-style dispatch hook a MIDI (or any other non-code) plugin could implement instead. The existing MIDI plugin (`muse/plugins/midi/plugin.py`) does have note-level content-ID concepts, but only for its own 3-way merge logic — nothing symlog could call into today. Building the generic domain-plugin symbol-extraction interface (and the MIDI-side implementation of it) is a substantial feature in its own right, not a quick add alongside this ticket's other items — genuinely still blocked, correctly left open rather than hacked around. ### 6. Relative time syntax (`@{1.day.ago}`, `@{yesterday}`) ✅ IMPLEMENTED `@{N}` addresses work by index. Git's reflog also supports relative time syntax (`HEAD@{1.hour.ago}`). Symlog `--since`/`--until` cover the filtering use case, but `@{1.day.ago}` as a direct ref specifier is more ergonomic in agent prompts. --- ## New items from implementation ### 7. `muse checkout-symbol "file.py::Symbol@{N}"` ✅ IMPLEMENTED Restore a prior version of a single symbol to disk without touching any other file. The natural extension of `muse code cat "addr@{N}"` — cat reads, checkout restores. Critically useful for surgical recovery: "undo the change to this one function without reverting the whole commit." **Note on command path**: this command already existed as `muse code checkout-symbol ADDRESS --commit REF` (not a bare top-level `muse checkout-symbol`, despite this ticket's heading) — it restores a symbol surgically into the working tree, splicing only that symbol's lines. The missing piece this item actually delivers: `ADDRESS` can now embed a symlog ref directly (`"file.py::Symbol@{N}"`), resolved via the existing `resolve_symlog_addr()`, making `--commit` optional. No new top-level alias was added — one name for one command, per workspace convention. ### 8. `muse symlog grep PATTERN [--file path] [--json]` ✅ IMPLEMENTED Search symbol bodies across their full symlog history. Answers: "when did this symbol last contain the string `deprecated`?" or "which version of this function called `session.add`?" Reads object store bodies via `resolve_symbol_body` for each entry. ### 9. `muse symlog show "file.py::Symbol@{N}"` ✅ IMPLEMENTED Convenience alias for `muse code cat "file.py::Symbol@{N}"` with human-focused output: operation, author, timestamp header, then the full symbol body. Useful for interactive exploration ("show me what this function looked like two changes ago"). ### 10. `muse symlog stat [--file path] [--json]` ✅ IMPLEMENTED Summary statistics for a symbol or file: total entries, operation breakdown (created/modified/deleted/renamed), most active author, first and last commit IDs, date range. Useful for code review context and agent decision-making. ### 11. Symlog entries for `muse revert` ✅ IMPLEMENTED `muse revert` records a reflog entry for the branch ref move, but writes no symlog entries for the symbols it touches. A revert of a commit that modified `compute_total` should produce a `symbol-modified: revert ` entry in that symbol's log. ### 12. `muse symlog prune-orphans [--dry-run] [--json]` ✅ IMPLEMENTED Find symlog files whose symbol address no longer exists in the current snapshot and remove them. Analogous to `muse gc` for unreachable objects — keeps the `.muse/symlogs/` tree clean after large-scale renames or file deletions.