diff options
| author | rottedfm <rottedfm@proton.me> | 2026-08-19 11:24:55 -0400 |
|---|---|---|
| committer | rottedfm <rottedfm@proton.me> | 2026-08-19 11:24:55 -0400 |
| commit | 8e16347b0eb329e84892af8ece36886324c95f62 (patch) | |
| tree | be1267972b5de2f1ae592577dfabce67f1fe6e87 /docs/traceability/trace.md | |
| parent | c6ae4660d1cc2414b22c492c5e819d009c8187c2 (diff) | |
| parent | ea0bd36167b684c0accdb5ce2b2e21b8d84aeb25 (diff) | |
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.md | 287 |
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`. |
