gabriel / muse public
symlog-followup.md markdown
186 lines 9.7 KB
Raw
sha256:b570925a487b44187c0441977d252906b6bf2719253bc53f96412d520cd24d9e feat: muse#53 item 4 — remote symlog sharing via push/pull/bundle Sonnet 5 minor ⚠ breaking 12 hours ago

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 <commit_id> 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.

File History 1 commit
sha256:b570925a487b44187c0441977d252906b6bf2719253bc53f96412d520cd24d9e feat: muse#53 item 4 — remote symlog sharing via push/pull/bundle Sonnet 5 minor ⚠ 12 hours ago