gabriel / muse public
feat patch symlog task/symlog-phase1 #3 / 3
AI Agent gabriel · 20 days ago · Jun 28, 2026 · Diff

feat(symlog): Phase 1 — core storage layer (SL_01–SL_12)

Add muse/core/symlog.py with the full Phase 1 storage primitives: - SymlogEntry dataclass (frozen, born_from derived from operation) - NULL_CONTENT_ID constant and is_null_content_id() helper - symlog_path() with percent-encoding and path-traversal rejection - append_symlog() with author/operation sanitization - read_symlog() with follow=True rename-chain traversal - list_symlog_symbols() and list_symlog_symbols_for_file() - expire_symlog() and delete_symlog_entry() — both atomic - _MAX_SYMLOG_BYTES file-size cap with warning

Add tests/test_core_symlog.py: 58 tests covering SL_01–SL_12, all green. Add paths.symlogs_dir() path helper. Add docs/issues/symlog.md (issue #51 body) and docs/issues/reflog-long-ids.md.

sha256:6986d42446334d306eb3136231a58606d896a0dcc796d29ac2ded0f9d1149ac0 sha
+166 symbols
sha256:6437019c392057e67058a0d2f0e71823999b9f399f9786b433cee3b2cb64e30e snapshot
+166
symbols added
0
dead code introduced
Semantic Changes 166 symbols
~ docs/issues/reflog-long-ids.md .md 8 symbols added
+ muse reflog — use long IDs in human text output section muse reflog — use long IDs in human text output L1–70
+ Deliverables section Deliverables L53–66
+ Fix section Fix L37–53
+ code variable variable code L49–52
+ Out of scope section Out of scope L66–70
+ Problem section Problem L3–37
+ code variable variable code L20–23
+ code[python] variable variable code[python] L8–17
~ docs/issues/symlog.md .md 27 symbols added
+ muse symlog — Live Per-Symbol Journal section muse symlog — Live Per-Symbol Journal L1–479
+ Acceptance criteria section Acceptance criteria L453–469
+ Background section Background L3–28
+ Why git genuinely cannot do this section Why git genuinely cannot do this L19–28
+ Entry format section Entry format L62–89
+ code@L64 variable variable code@L64 L64–67
+ code@L75 variable variable code@L75 L75–88
+ Goal section Goal L28–44
+ Out of scope section Out of scope L469–479
+ Phases section Phases L89–453
+ py) section Phase 1 — Core storage layer (muse/core/symlog.py) L93–147
+ code[python] variable variable code[python] L100–111
+ Phase 2 — Commit integration section Phase 2 — Commit integration L147–211
+ code variable variable code L155–171
+ Phase 3 — muse symlog read CLI section Phase 3 — muse symlog read CLI L211–299
+ code variable variable code L246–260
+ code[json] variable variable code[json] L218–241
+ Phase 4 — Lifecycle management section Phase 4 — Lifecycle management L299–369
+ code variable variable code L307–316
+ code[json]@L319 variable variable code[json]@L319 L319–329
+ code[json]@L332 variable variable code[json]@L332 L332–341
+ Phase 5 — @{N} ref resolution for symbols section Phase 5 — @{N} ref resolution for symbols L369–453
+ code[bash] variable variable code[bash] L386–399
+ code[json] variable variable code[json] L402–414
+ table section table L378–383
+ Storage layout section Storage layout L44–62
+ code variable variable code L46–57
~ muse/core/symlog.py .py 35 symbols added
+ NULL_CONTENT_ID variable variable NULL_CONTENT_ID L98–98
+ SymlogEntry class class SymlogEntry L154–173
+ __post_init__ method method __post_init__ L171–173
+ _BORN_FROM_RE variable variable _BORN_FROM_RE L108–108
+ _MAX_SYMLOG_BYTES variable variable _MAX_SYMLOG_BYTES L102–102
+ _SAFE_CHARS variable variable _SAFE_CHARS L105–105
+ _addr_from_path function function _addr_from_path L228–235
+ _decode_symbol_name function function _decode_symbol_name L125–127
+ _encode_symbol_name function function _encode_symbol_name L120–122
+ _make_entry function function _make_entry L176–192
+ _parse_line function function _parse_line L286–319
+ _reject_traversal function function _reject_traversal L130–136
+ _sanitize_author function function _sanitize_author L139–141
+ _sanitize_operation function function _sanitize_operation L144–146
+ append_symlog function function append_symlog L242–279
+ delete_symlog_entry function function delete_symlog_entry L521–588
+ expire_symlog function function expire_symlog L438–514
+ DEFAULT_HASH_ALGO import import DEFAULT_HASH_ALGO L88–88
+ _field import import _field L85–85
+ _symlogs_dir import import _symlogs_dir L87–87
+ annotations import import annotations L76–76
+ dataclass class import dataclass L85–85
+ datetime import import datetime L78–78
+ logging import import logging L79–79
+ os import import os L80–80
+ pathlib import import pathlib L81–81
+ re import import re L82–82
+ tempfile import import tempfile L83–83
+ parse import import urllib.parse L84–84
+ is_null_content_id function function is_null_content_id L115–117
+ list_symlog_symbols function function list_symlog_symbols L392–407
+ list_symlog_symbols_for_file function function list_symlog_symbols_for_file L410–431
+ logger variable variable logger L90–90
+ read_symlog function function read_symlog L326–385
+ symlog_path function function symlog_path L199–225
~ tests/test_core_symlog.py .py 95 symbols added
+ TestAppendSymlog class class TestAppendSymlog L187–289
+ test_author_newline_stripped method method test_author_newline_stripped L239–250
+ test_author_tab_stripped method method test_author_tab_stripped L252–262
+ test_creates_parent_dirs_and_file method method test_creates_parent_dirs_and_file L188–200
+ test_different_symbols_same_file_get_separate_files method method test_different_symbols_same_file_get_separate_files L277–289
+ test_line_format method method test_line_format L217–237
+ test_one_line_written_per_call method method test_one_line_written_per_call L202–215
+ test_operation_newline_stripped method method test_operation_newline_stripped L264–275
+ TestDeleteSymlogEntry class class TestDeleteSymlogEntry L545–612
+ _write_entries method method _write_entries L546–553
+ test_atomic_write_no_lock_file_left method method test_atomic_write_no_lock_file_left L599–604
+ test_delete_all_removes_file method method test_delete_all_removes_file L555–561
+ test_delete_index_zero_removes_newest method method test_delete_index_zero_removes_newest L563–572
+ test_delete_last_entry_removes_file method method test_delete_last_entry_removes_file L606–612
+ test_delete_middle_entry method method test_delete_middle_entry L574–582
+ test_missing_log_all_is_graceful method method test_missing_log_all_is_graceful L590–593
+ test_missing_log_by_index_raises method method test_missing_log_by_index_raises L595–597
+ test_out_of_bounds_raises_index_error method method test_out_of_bounds_raises_index_error L584–588
+ TestExpireSymlog class class TestExpireSymlog L479–537
+ _write_old method method _write_old L480–487
+ _write_recent method method _write_recent L489–495
+ test_all_expired_file_deleted method method test_all_expired_file_deleted L509–513
+ test_atomic_write method method test_atomic_write L522–529
+ test_dry_run_writes_nothing method method test_dry_run_writes_nothing L515–520
+ test_no_log_file_returns_zero_zero method method test_no_log_file_returns_zero_zero L497–499
+ test_old_entries_expired method method test_old_entries_expired L501–507
+ test_recent_entries_kept method method test_recent_entries_kept L531–537
+ TestIntegration class class TestIntegration L620–682
+ test_created_deleted_sentinel_fields method method test_created_deleted_sentinel_fields L665–682
+ test_five_entries_round_trip_newest_first method method test_five_entries_round_trip_newest_first L621–644
+ test_two_symbols_same_file_independent method method test_two_symbols_same_file_independent L646–663
+ TestListSymlogSymbols class class TestListSymlogSymbols L417–471
+ _write method method _write L418–424
+ test_empty_repo_returns_empty method method test_empty_repo_returns_empty L426–427
+ test_list_for_file_empty_dir_returns_empty method method test_list_for_file_empty_dir_returns_empty L460–462
+ test_list_for_file_returns_only_that_files_symbols method method test_list_for_file_returns_only_that_files_symbols L449–458
+ test_list_for_file_skips_symlinks method method test_list_for_file_skips_symlinks L464–471
+ test_returns_all_symbol_addresses_sorted method method test_returns_all_symbol_addresses_sorted L429–437
+ test_skips_symlinks method method test_skips_symlinks L439–447
+ TestNullContentId class class TestNullContentId L46–59
+ test_is_null_content_id_false_for_bare_zeros method method test_is_null_content_id_false_for_bare_zeros L57–59
+ test_is_null_content_id_false_for_real_id method method test_is_null_content_id_false_for_real_id L54–55
+ test_is_null_content_id_true method method test_is_null_content_id_true L51–52
+ test_null_content_id_format method method test_null_content_id_format L47–49
+ TestPathEncoding class class TestPathEncoding L137–179
+ test_colon_in_symbol_name_is_percent_encoded method method test_colon_in_symbol_name_is_percent_encoded L154–158
+ test_dotdot_in_file_path_raises method method test_dotdot_in_file_path_raises L160–162
+ test_dotdot_in_symbol_name_raises method method test_dotdot_in_symbol_name_raises L164–166
+ test_empty_symbol_name_raises method method test_empty_symbol_name_raises L172–174
+ test_missing_double_colon_raises method method test_missing_double_colon_raises L168–170
+ test_nested_path_maps_correctly method method test_nested_path_maps_correctly L143–145
+ test_plain_symbol_name_unchanged method method test_plain_symbol_name_unchanged L176–179
+ test_simple_symbol_maps_correctly method method test_simple_symbol_maps_correctly L138–141
+ test_special_chars_in_symbol_name_are_percent_encoded method method test_special_chars_in_symbol_name_are_percent_encoded L147–152
+ TestReadSymlog class class TestReadSymlog L297–409
+ _write_n_entries method method _write_n_entries L298–313
+ test_all_fields_round_trip method method test_all_fields_round_trip L334–351
+ test_born_from_parsed_on_read method method test_born_from_parsed_on_read L353–363
+ test_empty_log_returns_empty_list method method test_empty_log_returns_empty_list L315–317
+ test_entries_returned_newest_first method method test_entries_returned_newest_first L319–326
+ test_file_size_cap_warns_and_returns method method test_file_size_cap_warns_and_returns L384–400
+ test_follow_false_does_not_traverse method method test_follow_false_does_not_traverse L402–409
+ test_follow_traverses_rename_chain method method test_follow_traverses_rename_chain L365–382
+ test_limit_respected method method test_limit_respected L328–332
+ TestSymlogEntry class class TestSymlogEntry L67–129
+ _make_entry method method _make_entry L68–84
+ test_born_from_extracts_full_address method method test_born_from_extracts_full_address L126–129
+ test_born_from_none_for_regular_operation method method test_born_from_none_for_regular_operation L102–104
+ test_born_from_none_for_symbol_created method method test_born_from_none_for_symbol_created L111–116
+ test_born_from_none_for_symbol_renamed_to method method test_born_from_none_for_symbol_renamed_to L118–124
+ test_born_from_parsed_for_born_from_operation method method test_born_from_parsed_for_born_from_operation L106–109
+ test_frozen method method test_frozen L97–100
+ test_has_seven_fields method method test_has_seven_fields L86–95
+ _CID_A variable variable _CID_A L32–32
+ _CID_B variable variable _CID_B L33–33
+ _CID_C variable variable _CID_C L34–34
+ _CID_D variable variable _CID_D L35–35
+ _COMMIT_X variable variable _COMMIT_X L36–36
+ _COMMIT_Y variable variable _COMMIT_Y L37–37
+ _COMMIT_Z variable variable _COMMIT_Z L38–38
+ NULL_CONTENT_ID import import NULL_CONTENT_ID L18–18
+ SymlogEntry import import SymlogEntry L18–18
+ annotations import import annotations L6–6
+ append_symlog import import append_symlog L18–18
+ datetime import import datetime L8–8
+ delete_symlog_entry import import delete_symlog_entry L18–18
+ expire_symlog import import expire_symlog L18–18
+ is_null_content_id import import is_null_content_id L18–18
+ list_symlog_symbols import import list_symlog_symbols L18–18
+ list_symlog_symbols_for_file import import list_symlog_symbols_for_file L18–18
+ pathlib import import pathlib L9–9
+ pytest import import pytest L12–12
+ read_symlog import import read_symlog L18–18
+ symlog_path import import symlog_path L18–18
+ time import import time L10–10
~ muse/core/paths.py .py 1 symbol added
+ symlogs_dir function function symlogs_dir L76–78

0 comments

No comments yet. Be the first to start the discussion.

To add a comment, use the Muse CLI: muse hub commit comment sha256:6986d42446334d306eb3136231a58606d896a0dcc796d29ac2ded0f9d1149ac0 --body "your comment"