aboutsummaryrefslogtreecommitdiff
path: root/docs/traceability/trace.md
diff options
context:
space:
mode:
authorrottedfm <rottedfm@proton.me>2026-08-19 11:24:55 -0400
committerrottedfm <rottedfm@proton.me>2026-08-19 11:24:55 -0400
commit8e16347b0eb329e84892af8ece36886324c95f62 (patch)
treebe1267972b5de2f1ae592577dfabce67f1fe6e87 /docs/traceability/trace.md
parentc6ae4660d1cc2414b22c492c5e819d009c8187c2 (diff)
parentea0bd36167b684c0accdb5ce2b2e21b8d84aeb25 (diff)
Merge branch 'buffer-implementation'HEADmain
Establishes the first working baseline: moji <file> opens a file into a ropey rope and edits it with Helix selection-first modal editing, under a DO-178C DAL-C requirements and traceability process. Prior to this, main tracked four files and src/main.rs was still println!("Hello, world!") — there was no buildable state to build on. Verified on a fresh clone of the branch with no untracked files: cargo build; clippy --all-targets -D warnings clean; 294 tests passing; scripts/check-trace.sh reports 98/98 requirements traced in both directions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'docs/traceability/trace.md')
-rw-r--r--docs/traceability/trace.md287
1 files changed, 287 insertions, 0 deletions
diff --git a/docs/traceability/trace.md b/docs/traceability/trace.md
new file mode 100644
index 0000000..1b02e71
--- /dev/null
+++ b/docs/traceability/trace.md
@@ -0,0 +1,287 @@
+# mojibake — Traceability Matrix
+
+Software Level: **DAL-C** (DO-178C)
+
+Bidirectional trace: **HLR → LLR → source → test**. Source items carry a
+`// MJB-LLR-nnn` comment; tests are named `mjb_llr_nnn_<description>` so this
+matrix can be checked mechanically:
+
+```bash
+# every LLR tagged in source
+grep -rho 'MJB-LLR-[0-9]\+' src/ | sort -u
+# every LLR exercised by a test
+grep -rho 'mjb_llr_[0-9]\+' src/ tests/ | sort -u
+```
+
+Paths are relative to the repository root.
+
+---
+
+## HLR → LLR
+
+| HLR | Subject | LLRs |
+|---|---|---|
+| MJB-HLR-001 | File load from command line | 111, 112 |
+| MJB-HLR-002 | Encoding and BOM | 110, 111, 113, 115, 118 |
+| MJB-HLR-003 | Line ending preservation | 114, 115 |
+| MJB-HLR-004 | Rope text storage | 090–092, 117 |
+| MJB-HLR-005 | Selection model | 001–011, 020–025 |
+| MJB-HLR-006 | Character and line motion | 062, 063, 064 |
+| MJB-HLR-007 | Word motions select | 060, 061, 065–069 |
+| MJB-HLR-008 | Goto commands | 070–073, 150–154 |
+| MJB-HLR-009 | Insert-mode entry | 049, 117 |
+| MJB-HLR-010 | Text modification | 040–050, 117 |
+| MJB-HLR-011 | Undo and redo | 045, 046, 051–053 |
+| MJB-HLR-012 | Viewport pagination | 090–092, 200 |
+| MJB-HLR-013 | Scrolling and paging | 093–102 |
+| MJB-HLR-014 | TOML configuration | 180, 181 |
+| MJB-HLR-015 | Config-driven modal keymap | 150–157, 182, 184 |
+| MJB-HLR-016 | Command mode | 158, 159, 160 |
+| MJB-HLR-017 | File write | 130–137 |
+| MJB-HLR-018 | Error handling | 042, 044, 118, 183 |
+| MJB-HLR-019 | Single-widget presentation | 200–205 |
+
+---
+
+## LLR → source → test
+
+### Selection (`src/buffer/selection.rs`)
+
+| LLR | Source item | Test |
+|---|---|---|
+| 001 | `Range` | `mjb_llr_001_offsets_are_byte_indices` |
+| 002 | `Range::from`, `Range::to` | `mjb_llr_002_from_and_to_ignore_direction` |
+| 003 | `Range::is_empty` | `mjb_llr_003_is_empty` |
+| 004 | `Range::direction` | `mjb_llr_004_direction` |
+| 005 | `Range::cursor` | `mjb_llr_005_cursor_steps_back_on_forward_range`, `mjb_llr_005_cursor_respects_grapheme_clusters` |
+| 006 | `Range::put_cursor` | `mjb_llr_006_put_cursor_without_extend_collapses` |
+| 007 | `Range::put_cursor` | `mjb_llr_007_put_cursor_with_extend_keeps_anchor`, `mjb_llr_007_put_cursor_extend_flips_direction`, `mjb_llr_007_select_mode_motions_extend` |
+| 008 | `Range::line_range` | `mjb_llr_008_line_range` |
+| 009 | `Selection` | `mjb_llr_009_selection_invariant` |
+| 010 | `Selection::primary` | `mjb_llr_010_primary_round_trips` |
+| 011 | `Range::clamped` | `mjb_llr_011_clamped_snaps_into_bounds_and_onto_char_boundary`, `mjb_llr_011_motions_at_boundaries_never_leave_the_buffer` |
+
+### Graphemes (`src/buffer/grapheme.rs`)
+
+| LLR | Source item | Test |
+|---|---|---|
+| 020 | `prev_grapheme_boundary` | `mjb_llr_020_prev_boundary_saturates_at_zero`, `mjb_llr_020_combining_mark_is_one_cluster` |
+| 021 | `next_grapheme_boundary` | `mjb_llr_021_next_boundary_saturates_at_end`, `mjb_llr_021_multibyte_advances_whole_char` |
+| 022 | chunk-walking loops in both | `mjb_llr_022_boundaries_resolve_across_chunk_edges` |
+| 023 | `is_grapheme_boundary` | `mjb_llr_023_boundary_detection` |
+| 024 | `grapheme_width` | `mjb_llr_024_widths` |
+| 025 | `display_column` | `mjb_llr_025_display_column_counts_width_not_bytes`, `mjb_llr_025_display_column_tab_expands` |
+
+### Transactions and history (`src/buffer/transaction.rs`, `history.rs`)
+
+| LLR | Source item | Test |
+|---|---|---|
+| 040 | `Operation` | `mjb_llr_040_operation_counts_are_byte_lengths` |
+| 041 | `ChangeSet` | `mjb_llr_041_changeset_records_both_lengths`, `mjb_llr_041_empty_changeset_reports_equal_lengths` |
+| 042 | `ChangeSet::apply` length check | `mjb_llr_042_length_mismatch_is_rejected`, `mjb_llr_042_rejected_change_leaves_rope_untouched` |
+| 043 | `ChangeSet::apply` | `mjb_llr_043_apply_insert_and_delete` |
+| 044 | `ChangeSet::apply` boundary check | `mjb_llr_044_non_char_boundary_errors_rather_than_panics` |
+| 045 | `ChangeSet::invert` | `mjb_llr_045_invert_maps_each_operation` |
+| 046 | `apply` + `invert` | `mjb_llr_046_apply_then_invert_round_trips` |
+| 047 | `Transaction` | `mjb_llr_047_transaction_carries_a_selection` |
+| 048 | `Transaction::change` | `mjb_llr_048_multiple_ordered_changes` |
+| 049 | `Transaction::insert` | `mjb_llr_049_insert_at_cursor` |
+| 050 | `Transaction::delete` | `mjb_llr_050_delete_selection_span`, `mjb_llr_050_d_with_an_empty_selection_deletes_one_grapheme`, `mjb_llr_050_d_on_an_empty_buffer_is_a_noop` |
+| 051 | `History::commit` | `mjb_llr_051_commit_then_undo_then_redo`, `mjb_llr_051_commit_after_undo_discards_the_redo_branch`, `mjb_llr_051_undo_restores_typed_text` |
+| 052 | `History::undo`, `Document::undo` | `mjb_llr_052_undo_past_start_is_a_noop`, `mjb_llr_052_undo_past_history_start_is_a_noop`, `mjb_llr_052_undo_past_history_start_is_safe`, `mjb_llr_052_u_undoes_a_deletion` |
+| 053 | `History::redo`, `Document::redo` | `mjb_llr_053_redo_past_end_is_a_noop`, `mjb_llr_053_redo_past_end_is_safe`, `mjb_llr_053_capital_u_redoes` |
+
+### Movement (`src/buffer/movement.rs`)
+
+| LLR | Source item | Test |
+|---|---|---|
+| 060 | `CharCategory`, `categorize_char` | `mjb_llr_060_categories` |
+| 061 | `is_word_boundary` | `mjb_llr_061_word_boundary_is_category_change` |
+| 062 | `move_char_left` | `mjb_llr_062_move_char_left_stops_at_zero`, `mjb_llr_062_h_and_l_move_by_one_grapheme`, `mjb_llr_062_h_at_start_of_buffer_is_a_noop` |
+| 063 | `move_char_right` | `mjb_llr_063_move_char_right_stops_at_end`, `mjb_llr_063_move_char_right_skips_whole_multibyte_char`, `mjb_llr_063_l_at_end_of_buffer_is_a_noop` |
+| 064 | `move_vertically` | `mjb_llr_064_vertical_motion_preserves_column`, `mjb_llr_064_vertical_motion_clamps_to_short_line`, `mjb_llr_064_vertical_motion_is_noop_at_edges`, `mjb_llr_064_j_and_k_move_between_lines` |
+| 065 | `next_word_start` | `mjb_llr_065_next_word_start_produces_a_selection`, `mjb_llr_065_w_leaves_a_selection`, `mjb_llr_065_w_then_d_deletes_the_word`, `mjb_llr_065_counted_next_word_start_advances_once_per_count` |
+| 066 | `prev_word_start` | `mjb_llr_066_prev_word_start_spans_backward`, `mjb_llr_066_b_selects_backward`, `mjb_llr_066_counted_prev_word_start_advances_once_per_count` |
+| 067 | `next_word_end` | `mjb_llr_067_next_word_end_spans_the_word`, `mjb_llr_067_e_selects_to_the_word_end`, `mjb_llr_067_counted_next_word_end_advances_once_per_count` |
+| 068 | `is_separator` and the skip loops in the three word functions | `mjb_llr_068_word_motion_stops_at_punctuation`, `mjb_llr_068_long_word_motion_absorbs_punctuation`, `mjb_llr_068_word_motion_crosses_line_endings`, `mjb_llr_068_capital_w_treats_punctuation_as_word_characters` |
+| 069 | `word_move` boundary guards | `mjb_llr_069_word_motion_is_noop_at_boundaries`, `mjb_llr_069_w_at_end_of_buffer_is_a_noop`, `mjb_llr_069_counted_motion_saturates_at_the_buffer_end` |
+| 070 | `goto_file_start` | `mjb_llr_070_goto_file_start`, `mjb_llr_070_gg_goes_to_file_start` |
+| 071 | `goto_last_line` | `mjb_llr_071_goto_last_line`, `mjb_llr_071_goto_last_line_without_trailing_newline`, `mjb_llr_071_ge_goes_to_the_last_line` |
+| 072 | `goto_line_start` | `mjb_llr_072_goto_line_start`, `mjb_llr_072_gh_goes_to_the_line_start`, `mjb_llr_072_gs_goes_to_first_non_whitespace` |
+| 073 | `goto_line_end`, `line_end_byte` | `mjb_llr_073_goto_line_end_excludes_terminator`, `mjb_llr_073_goto_line_end_handles_crlf`, `mjb_llr_073_gl_goes_to_the_line_end` |
+
+### Viewport (`src/buffer/view.rs`)
+
+| LLR | Source item | Test |
+|---|---|---|
+| 090 | `ViewPosition` | `mjb_llr_090_anchor_is_a_byte_offset_at_a_line_start` |
+| 091 | `View::top_line` | `mjb_llr_091_top_line_from_anchor` |
+| 092 | `View::visible_lines`, `visible_line_range` | `mjb_llr_092_visible_lines_are_bounded_by_height`, `mjb_llr_092_visible_lines_clamp_near_end_of_buffer`, `mjb_llr_092_empty_buffer_renders_safely`, `mjb_llr_092_visible_line_count_is_independent_of_file_size`, `mjb_llr_092_scrolling_a_large_file_stays_responsive` |
+| 093 | scroll-off clamping | `mjb_llr_093_scrolloff_larger_than_viewport_is_clamped` |
+| 094 | scroll-up branch | `mjb_llr_094_scrolls_up_to_honour_top_margin`, `mjb_llr_094_scroll_near_start_saturates_at_zero` |
+| 095 | scroll-down branch | `mjb_llr_095_scrolls_down_to_honour_bottom_margin` |
+| 096 | no-scroll branch | `mjb_llr_096_no_scroll_when_cursor_is_comfortable` |
+| 097 | `View::set_top_line` | `mjb_llr_097_top_line_clamps_into_buffer` |
+| 098 | zero-height guard | `mjb_llr_098_zero_height_viewport_is_a_noop`, `mjb_llr_098_zero_height_viewport_does_not_panic` |
+| 099 | `ensure_horizontal_in_view` | `mjb_llr_099_horizontal_scroll_follows_cursor` |
+| 100 | `View::page` (half) | `mjb_llr_100_half_page_moves_cursor_and_view`, `mjb_llr_100_ctrl_d_pages_half_a_screen_down`, `mjb_llr_100_ctrl_u_pages_back_up` |
+| 101 | `View::page` (full) | `mjb_llr_101_full_page_moves_by_height`, `mjb_llr_101_ctrl_f_pages_a_full_screen_down` |
+| 102 | paging saturation | `mjb_llr_102_paging_saturates_at_both_ends` |
+
+### Document, encoding, line endings
+
+| LLR | Source item | Test |
+|---|---|---|
+| 110 | `encoding::detect_bom` | `mjb_llr_110_detects_each_bom` |
+| 111 | `Document::open`, `Buffer::new` | `mjb_llr_111_loads_contents`, `mjb_llr_111_plain_utf8_round_trips`, `mjb_llr_111_no_path_yields_a_scratch_buffer` |
+| 112 | `Document::open` not-found arm | `mjb_llr_112_missing_file_yields_empty_buffer_that_remembers_the_path`, `mjb_llr_112_empty_file_is_editable`, `mjb_llr_112_file_without_trailing_newline_round_trips`, `mjb_llr_112_writing_a_new_file_creates_it`, `mjb_llr_112_missing_file_opens_as_an_empty_buffer` |
+| 113 | `encoding::decode` non-UTF-8 branch | `mjb_llr_113_declared_utf16_is_transcoded_not_rejected`, `mjb_llr_113_empty_input_decodes_to_empty`, `mjb_llr_113_declared_utf16_file_opens`, `mjb_llr_113_declared_utf16_file_opens_and_edits` |
+| 118 | `DecodeError`, `encoding::decode` UTF-8 branch, `Document::open` | `mjb_llr_118_invalid_utf8_is_rejected`, `mjb_llr_118_rejection_names_the_offset`, `mjb_llr_118_truncated_multibyte_char_is_rejected`, `mjb_llr_118_declared_utf8_is_strict_too`, `mjb_llr_118_valid_multibyte_utf8_is_accepted`, `mjb_llr_118_invalid_utf8_file_is_refused`, `mjb_llr_118_binary_file_is_refused_not_opened` |
+| 114 | `LineEnding::detect` | `mjb_llr_114_detects_lf`, `_detects_crlf`, `_detects_lone_cr`, `_falls_back_to_platform_default`, `_first_terminator_decides`, `_crlf_is_detected_and_normalized_for_storage` |
+| 115 | `encoding::encode`, `Document::encode` | `mjb_llr_115_bom_round_trips`, `mjb_llr_115_utf16le_round_trips`, `mjb_llr_115_utf16be_round_trips`, `mjb_llr_115_utf16_handles_non_ascii_and_surrogates`, `mjb_llr_115_apply_restores_original_ending`, `mjb_llr_115_crlf_round_trips_through_save` |
+| 116 | `Document::is_modified`, `History::revision` | `mjb_llr_116_modified_flag_lifecycle`, `mjb_llr_116_revision_is_not_stack_depth`, `mjb_llr_116_revision_is_zero_when_pristine_and_returns_on_undo`, `mjb_llr_116_undo_back_to_saved_state_is_clean`, `mjb_llr_116_divergent_edit_at_the_same_depth_stays_modified` |
+| 117 | `Document::apply` | `mjb_llr_117_apply_updates_text_and_records_history` |
+
+### Save path (`src/buffer/save.rs`)
+
+| LLR | Source item | Test |
+|---|---|---|
+| 130 | `resolve_write_path` | `mjb_llr_130_write_follows_symlink_without_replacing_it`, `mjb_llr_130_relative_symlink_resolves_against_its_own_directory`, `mjb_llr_130_write_through_a_symlink_end_to_end` |
+| 131 | `readonly` | `mjb_llr_131_readonly_target_is_refused`, `mjb_llr_131_missing_file_is_not_readonly`, `mjb_llr_131_readonly_file_reports_and_preserves_contents` |
+| 132 | missing-parent branch | `mjb_llr_132_missing_parent_is_refused_without_force`, `mjb_llr_132_force_creates_the_parent` |
+| 133 | `must_copy` | `mjb_llr_133_hardlink_is_detected_and_preserved`, `mjb_llr_133_plain_file_does_not_need_copy_mode` |
+| 134 | `backup_path`, backup creation | `mjb_llr_134_backup_is_created_beside_the_target`, `mjb_llr_134_backup_path_handles_a_bare_file_name` |
+| 135 | restore-on-failure | `mjb_llr_135_failed_write_restores_the_original` |
+| 136 | `copy_permissions`, backup removal | `mjb_llr_136_no_backup_file_is_left_behind` |
+| 137 | `Document::save` | `mjb_llr_116_modified_flag_lifecycle` |
+
+### Keymap and commands (`src/buffer/keymap.rs`, `mod.rs`)
+
+| LLR | Source item | Test |
+|---|---|---|
+| 150 | `Keymap::new` prefix set | `mjb_llr_150_proper_prefixes_are_precomputed`, `mjb_llr_150_prefix_set_is_empty_for_a_mode_without_sequences` |
+| 151 | `Keymap::resolve` match arm | `mjb_llr_151_single_key_binding_matches_immediately`, `mjb_llr_151_two_key_sequence_resolves`, `mjb_llr_151_sequences_sharing_a_prefix_stay_distinct` |
+| 152 | `Keymap::resolve` prefix arm | `mjb_llr_152_prefix_key_waits_for_more` |
+| 153 | `Keymap::resolve` cancel arm | `mjb_llr_153_unknown_continuation_cancels`, `mjb_llr_153_unbound_key_cancels_immediately`, `mjb_llr_153_unknown_g_sequence_does_not_corrupt_state` |
+| 154 | absence of any timer | `mjb_llr_154_pending_is_not_discarded_by_time`, `mjb_llr_154_gg_resolves_regardless_of_intervening_time` |
+| 155 | count accumulation | `mjb_llr_155_count_accumulates_and_is_delivered`, `_count_is_consumed_once`, `_leading_zero_is_not_a_count`, `_zero_extends_an_existing_count`, `_digits_are_not_counts_in_insert_mode`, `_count_repeats_a_motion` |
+| 156 | `self_insert_char` | `mjb_llr_156_self_insert_accepts_plain_printables`, `_rejects_control_and_alt`, `_rejects_non_char_and_sequences`, `_i_then_typing_inserts_text`, `_command_keys_type_literally_in_insert_mode`, `_unbound_control_key_is_discarded_in_insert_mode` |
+| 157 | `Command` enum | `mjb_llr_157_deserializes_from_the_variant_name`, `mjb_llr_157_unknown_command_name_is_an_error_not_a_panic`, `mjb_llr_157_display_round_trips_through_deserialization` |
+| 158 | `Buffer::handle_command_mode_key` | `mjb_llr_158_escape_cancels_the_command_line`, `_command_line_accepts_backspace`, `_backspacing_an_empty_command_line_leaves_command_mode` |
+| 159 | `Buffer::run_command_line` | `mjb_llr_159_write_saves_to_disk`, `_quit_returns_the_quit_outcome`, `_wq_writes_then_quits`, `_x_is_an_alias_for_wq`, `_unknown_command_is_reported_not_fatal`, `_force_write_creates_missing_directories`, `_empty_command_line_does_nothing` |
+| 160 | unsaved-changes guard | `mjb_llr_160_quit_with_unsaved_changes_is_refused`, `mjb_llr_160_force_quit_discards_changes` |
+
+### Configuration (`src/config.rs`)
+
+| LLR | Source item | Test |
+|---|---|---|
+| 180 | `CONFIG`, `Config::new` | `mjb_llr_180_builtin_defaults_parse` |
+| 181 | single TOML source | `mjb_llr_181_only_config_toml_is_read` + `cargo tree` check (see below) |
+| 182 | default-merge loop | `mjb_llr_182_user_bindings_merge_per_binding` |
+| 183 | `KeyBindings::deserialize`, `parse_color` | `mjb_llr_183_invalid_keybinding_is_recoverable`, `mjb_llr_183_malformed_colours_never_panic`, `mjb_llr_183_colour_cube_bounds`, `mjb_llr_183_grayscale_ramp_bounds`, `mjb_llr_183_bright_colour_is_base_plus_eight` |
+| 184 | `Mode` | `mjb_llr_184_modes_deserialize_lowercase`, `mjb_llr_184_v_toggles_select_mode` |
+| 185 | `EditorConfig` | `mjb_llr_185_editor_defaults`, `mjb_llr_185_final_newline_only_added_when_missing`, `mjb_llr_185_final_newline_added_on_encode_when_requested`, `mjb_llr_185_empty_document_encodes_empty` |
+
+### Presentation (`src/components/buffer.rs`, `src/app.rs`)
+
+Rendering is tested against a real cell grid via ratatui's `TestBackend`
+(`tests/rendering.rs`), not asserted by inspection.
+
+| LLR | Source item | Test |
+|---|---|---|
+| 200 | `BufferComponent::draw` render loop | `mjb_llr_200_renders_the_file_contents`, `mjb_llr_200_renders_only_the_visible_window`, `mjb_llr_200_line_numbers_are_shown_in_the_gutter`, `mjb_llr_200_empty_file_renders_without_panicking`, `mjb_llr_200_wide_characters_render` |
+| 201 | cursor/selection styling in `draw` | `mjb_llr_201_cursor_is_styled_distinctly` |
+| 202 | `status_line`, `message_line` | `mjb_llr_202_status_line_shows_mode_and_path`, `mjb_llr_202_status_line_marks_an_unmodified_file`, `mjb_llr_202_scratch_buffer_is_labelled` |
+| 203 | `App::handle_global_key`, `Keymap::lookup_single` | `mjb_llr_203_lookup_single_does_not_disturb_pending`, `mjb_llr_203_globally_bound_key_is_not_typed_as_text` |
+| 204 | `App::new` component vec | `mjb_llr_204_no_fps_counter_or_hello_world_is_rendered` |
+| 205 | absence of `last_tick_key_events` | `mjb_llr_205_pending_chord_survives_redraws` |
+
+`mjb_llr_200_renders_only_the_visible_window` is the direct check on
+MJB-HLR-012: a 1000-line file in an 8-row terminal must not put line 500 on
+screen.
+
+---
+
+## Completeness check
+
+The matrix above is verified mechanically, not by inspection:
+
+```bash
+grep -oE 'MJB-LLR-[0-9]+' docs/requirements/llr.md | sort -u > defined
+grep -rhoE 'MJB-LLR-[0-9]+' src/ | sort -u > tagged
+grep -rhoE 'mjb_llr_[0-9]+' src/ tests/ | sed 's/mjb_llr_/MJB-LLR-/' | sort -u > tested
+comm -23 defined tagged # LLRs with no source tag
+comm -23 defined tested # LLRs with no test
+comm -13 defined tested # tests naming an LLR that does not exist
+```
+
+Result at time of writing — all three sets empty:
+
+```
+defined: 98 tagged: 98 tested: 98
+```
+
+Every low-level requirement is tagged in source and exercised by at least one
+test named for it. Test totals: **282 passing** (179 unit, 89 editing
+integration, 14 rendering integration).
+
+---
+
+## Coverage
+
+Statement (line) coverage of the buffer core, the DAL-C structural criterion:
+
+```
+src/buffer/** statement coverage: 2665/2770 = 96.21%
+```
+
+Measured with `cargo llvm-cov --summary-only test`. Per-module figures:
+
+| Module | Lines | Covered |
+|---|---|---|
+| `buffer/view.rs` | 233 | 99.57% |
+| `buffer/transaction.rs` | 265 | 98.87% |
+| `buffer/encoding.rs` | 140 | 98.57% |
+| `buffer/movement.rs` | 346 | 97.98% |
+| `buffer/selection.rs` | 182 | 97.80% |
+| `buffer/keymap.rs` | 223 | 97.76% |
+| `buffer/history.rs` | 90 | 96.67% |
+| `buffer/command.rs` | 28 | 96.43% |
+| `buffer/document.rs` | 268 | 94.40% |
+| `buffer/mod.rs` | 408 | 93.87% |
+| `buffer/save.rs` | 218 | 93.12% |
+| `buffer/line_ending.rs` | 87 | 89.66% |
+| `buffer/grapheme.rs` | 195 | 89.23% |
+
+**MC/DC and decision coverage are not DAL-C objectives and are not claimed.**
+Only statement coverage is reported.
+
+### Uncovered code, and why
+
+- `buffer/grapheme.rs` (89.23%) — the lowest figure. The uncovered lines are
+ defensive `Err(_) => return` arms for `GraphemeIncomplete` variants that
+ cannot arise from a cursor constructed over the whole slice. They exist so a
+ malformed state degrades instead of panicking (MJB-HLR-018), and are
+ unreachable by construction, so no test can drive them.
+- `buffer/save.rs` (93.12%) — the restore-on-failure path is reachable only
+ when the process cannot write to a directory it owns. Running the suite as
+ root defeats the permission bits, so
+ `mjb_llr_135_failed_write_restores_the_original` skips its assertion in that
+ case rather than reporting a false pass. The path is covered when the suite
+ runs as an ordinary user.
+- `buffer/line_ending.rs` (89.66%) — `LineEnding::platform_default` has one
+ arm per platform; only the host's arm executes.
+- Remaining gaps are `Display`/`From` glue generated by `thiserror` on error
+ variants that no test provokes.
+
+### Verification not expressible as a unit test
+
+`cargo tree` confirms MJB-LLR-181 and MJB-HLR-014 — that no parser for a
+non-TOML configuration format remains in the dependency graph:
+
+```bash
+cargo tree | grep -iE "json|yaml|ini\b" # returns nothing
+```
+
+Confirmed: `config v0.15.25` resolves with only `pathdiff`, `serde_core`,
+`toml`, and `winnow`.