Skip to content

Split 300-line files into focused modules (Phase 7.2)#33

Merged
kevincarlson merged 72 commits into
mainfrom
claude/quality-pass-perf-cleanup-ecgj28
Jul 13, 2026
Merged

Split 300-line files into focused modules (Phase 7.2)#33
kevincarlson merged 72 commits into
mainfrom
claude/quality-pass-perf-cleanup-ecgj28

Conversation

@kevincarlson

Copy link
Copy Markdown
Member

Summary

This is a large refactoring pass that splits 43+ production files exceeding the 300-line ceiling into focused, single-responsibility modules. The changes enforce the file-ceiling convention mechanically (via CI) and reduce the worst offenders from 600+ lines to ≤300 lines each.

Key Changes

Layout engine (loki-layout)

  • flow.rs: Split into flow_entry.rs, flow_run.rs, flow_dispatch.rs, flow_tail.rs, flow_group.rs, flow_headers.rs
  • flow_para.rs: Split into flow_para_chain.rs, flow_split.rs, flow_para_place.rs, flow_para_between.rs
  • para.rs: Split into para_build.rs, para_layout_types.rs, para_types.rs, para_emit.rs (existing)
  • resolve.rs: Split into resolve_char_span.rs, resolve_inlines.rs, resolve_walk.rs
  • lib.rs: Added layout_entry module for top-level entry points

ODF reader/writer (loki-odf)

  • odt/reader/styles.rs: Split into styles_props.rs, styles_list_style.rs, styles_auto.rs, styles_table.rs
  • odt/reader/document.rs: Split into document_body.rs, document_list.rs, document_toc.rs
  • odt/model/styles.rs: Split into styles_props.rs (model types)
  • odt/write/mod.rs: Added default_style.rs, meta.rs modules
  • package.rs: Split into package_read.rs for ZIP-entry helpers
  • ods/import.rs: Split into ods/import_helpers.rs
  • ods/export.rs: Split into ods/export_content.rs

OOXML reader/writer (loki-ooxml)

  • docx/import.rs: Split into docx/import_package.rs for parse-and-map pipeline
  • docx/mapper/document.rs: Split into docx/mapper/document_page.rs
  • docx/mapper/inline.rs: Refactored (imports reduced)
  • docx/model/paragraph.rs: Split into docx/model/paragraph_run.rs
  • docx/omml/read.rs: Split into docx/omml/read_structs.rs
  • docx/omml/write.rs: Split into docx/omml/write_structs.rs
  • docx/reader/document.rs: Split into document_drawing.rs, document_ppr.rs, document_run.rs
  • docx/write/document.rs: Split into document_blocks.rs, document_drawing.rs, document_inlines.rs, document_para.rs
  • xlsx/export.rs: Split into xlsx/export_xml.rs
  • xlsx/import.rs: Split into xlsx/import_styles.rs, xlsx/import_worksheet.rs

Model and conformance

  • loki-doc-model: Added style/table_cnf.rs (explicit conditional formatting)
  • appthere-conformance: Promoted corpus vocabulary from loki-acid into corpus/mod.rs, corpus/manifest.rs, corpus/catalog/mod.rs
  • loki-acid: Refactored catalog/mod.rs and severity.rs to use new conformance corpus

UI and shell

  • appthere-ui: Added tokens/palette.rs (theme-variant color palette), components/template_browser.rs
  • loki-app-shell: Added spell/personal_dict.rs, tabs_tests.rs
  • loki-text: Split editor state into

https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo

claude added 30 commits July 11, 2026 03:14
Fixes the invalid-pointer-dereference advisory in crossbeam-epoch's
fmt::Pointer impl. Semver-compatible lockfile-only bump; the remaining
cargo-audit findings are the three documented transitive quick-xml
copies still blocked on upstream releases (see CLAUDE.md tech debt).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Performance (loki-layout / loki-vello / loki-ooxml):
- para_emit: build the per-glyph scale/rise vectors only when a span
  actually carries w:w scale or w:position baseline-shift — three heap
  allocations avoided per glyph run on the common path; resolve the
  covering span once per run instead of re-scanning spans for each of
  the 6 per-run attributes (link, super/sub, highlight, shadow,
  underline/strike variants).
- para_cache: stream the Debug-format cache-key bytes straight into the
  hasher via a fmt::Write adapter instead of allocating a String per
  paragraph per layout pass (paid even on cache hits).
- flow/flow_para: new para_keep_with_next() probe resolves only the
  keep-with-next flag for the paginated-loop pre-check and chain scan,
  replacing two full ParaProps clone+merge+map resolves per paragraph.
- xlsx import: single-pass <c> attribute read (local_attr_vals) instead
  of three scans per cell; cell_ref_to_coord is now allocation-free.
- FontDataCache: cache variable-font normalized coords as F2Dot14 bits
  (Vec<i16>) so paint_glyph_run stops rebuilding the vector per run.

Dead code:
- Remove unreferenced OOXML helpers (bool_attr, twips_to_points,
  half_points_to_points, emu_to_points, parse_twips) + their tests and
  the four unit constants only they used — production code uses the
  mapper-local conversions.
- Remove the four span_*_for_range helpers superseded by the single
  covering-span lookup.
- Delete tests/diagnose_import.rs — an assertion-free println debugging
  harness (per the no-committed-diagnostics rule); its fixture stays
  (bookmark_compat.rs uses it).

Unused dependencies removed:
- loki-text: dirs, serde, serde_json, vello, peniko, kurbo,
  wgpu_context, anyrender_vello, loki-vello (persistence + GPU surface
  moved to loki-app-shell/loki-renderer; comments were stale).
- loki-spreadsheet / loki-presentation: same stale graphics stack plus
  unused model/layout deps (verified no source references, and the
  android_main! macro expansion only touches deps that remain).
- appthere-ui: thiserror; loki-renderer: tokio; loki-server-store:
  serde, tracing; loki-server-api: base64.
- fontique kept everywhere (documented feature-unification hack).

Clippy (test targets): fix needless-borrow/collapsible-if in
loki-vello's visual_conformance and needless lifetimes in two loki-text
test helpers.

Docs: ratchet file-ceiling baseline (para.rs 1401, xlsx/import.rs 574),
sync CLAUDE.md sizes and the 2026-07-11 audit re-check note.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
clippy --all-targets flags runtime assert! on compile-time-constant
token comparisons; const { assert!(..) } checks them at compile time
instead. Test-target-only lint; the library gate was already clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
- Replace default()-then-assign with struct-literal initializers across
  the Loro bridge/mutation/style test files (field_reassign_with_default).
- Collapse nested if-lets in bookmark_tests (collapsible_if).
- Move tests/helpers.rs to tests/helpers/mod.rs so cargo stops compiling
  the shared helper module as its own integration-test crate (which
  flagged every helper as dead code under --all-targets linting).
- Drop a useless String into() in table_structural_ops.

The library clippy gate was already clean; this extends cleanliness to
test targets, which CI does not yet lint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
loki-ooxml and loki-odf opt into clippy::pedantic at the crate root, but
their #[path]-included test modules were never linted (the CI gate runs
without --all-targets). Fixes: concrete T::default() instead of
Default::default() in struct literals, struct-literal init instead of
default()-then-assign, digit separators in EMU literals, hoisted a
mid-function use, named the only other enum variant instead of a
wildcard arm, and clippy --fix's mechanical map_or/collapsible-if
cleanups. Scoped allows with comments where the lint fights test idiom:
float_cmp on exact-representable point conversions, cast_precision_loss
on u8-range hex components, type_complexity on two fixture bundles, and
dead_code on the shared tests/helpers module (each integration-test
crate uses a different subset).

cargo clippy --workspace --all-targets -- -D warnings is now fully
clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
The previous pass replaced Default::default() with concrete
T::default() calls in test files whose glob imports did not actually
bring those types into scope; import CharProps / IndexMap / NodeAttr
explicitly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…sture (7.4)

Closes the four audit-2026-06 security tails (deferred-features plan 7.4):

- S-1b: nested DOCX tables and content controls now carry a shared
  MAX_NESTING_DEPTH (100) budget through parse_table -> row -> cell and
  parse_sdt recursion, returning a typed OoxmlError::NestingTooDeep
  instead of risking stack exhaustion on crafted documents. Guarded
  positive (2-deep parses) and negative (150-deep rejected) tests.
- S-2: emu_to_pt clamps its raw i64 input to +/-100m of EMU so an absurd
  cx/cy attribute cannot flow unbounded into layout arithmetic.
- S-3: transcode_utf16_to_utf8 rejects an odd-length UTF-16 payload
  instead of silently truncating the trailing byte.
- S-5: the no-external-entity posture is documented at the XML entry
  modules of all three parsing crates and locked by a behavioral
  regression test (DOCTYPE entity stays literal, never resolved).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
The ODT writer emits <style:style style:family="table"> with
style:table-properties (width / alignment / background), but import only
restored the table's style-name reference — the definition was
written-but-not-re-read, so a round-tripped document lost its table
styles' geometry.

- reader: parse_style_props now collects style:table-properties via a
  new styles_table.rs submodule and returns a bundled ParsedStyleProps
  struct instead of a 5-tuple (retiring the clippy::type_complexity
  allow; both baselined files shrank and the ratchet was tightened).
- model: OdfStyle.table_props / OdfTableProps (raw attribute strings,
  mirroring OdfCellProps).
- mapper: a new OdfStyleFamily::Table arm maps the definition into
  catalog.table_styles via map_table_style_props — the exact inverse of
  write/table_style.rs (style:width / style:rel-width / table:align /
  fo:background-color).

Tests: 2 mapper unit tests; table_style_name_reference_round_trips now
asserts the definition (width/alignment/background) survives the full
export -> import cycle, plus a percent-width round-trip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Word semantics (ECMA-376 17.3.1.4): consecutive paragraphs with
identical border settings form one bordered group — previously each
member drew its own full box (doubled rules at every boundary) and
w:between was imported but never rendered.

New flow_para_between.rs decides group membership with a cheap
child-wins probe of the five border fields (direct formatting, then the
style parent chain — the para_keep_with_next pattern, no full resolve),
comparing the block about to flow with its slice neighbours. The block
loops stage the resulting override on FlowState; flow_paragraph applies
it to the resolved top/bottom edges before layout, so the adjustment
participates in the paragraph-cache key. The boundary rule (w:between,
converted like any border edge) draws once as the upper member's bottom
edge; a between-only group draws just the boundary rule.

Scope: StyledPara runs within one block slice (top level or one nested
container); keep-with-next chains and synthesized paragraphs break a
group. The four ratcheted flow/para files were held at baseline via
comment tightening; fidelity-status.md and the deferred-features plan
updated.

Tests: same_border_group_draws_between_rule_once,
different_borders_do_not_group,
between_only_group_draws_just_the_boundary_rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…re-colors (4c.4)

ThemeVariant::Light existed as a name only — no light values, no
consumer of use_theme(), no way to switch. Now:

- tokens/palette.rs: ThemePalette carries one field per color token;
  dark() is built from the COLOR_* constants (zero drift by
  construction), light() is the inverted set (chrome surfaces lighten,
  on-chrome text darkens, accents deepen for contrast; the document
  page, error banner, and page-side tokens stay shared). The constants
  remain the dark values so all ~400 existing call sites render
  unchanged and migrate opportunistically.
- theme.rs: AtThemeContext is now Signal-backed with palette()/toggle();
  the variant->palette mapping lives on ThemeVariant as pure, tested
  methods. use_theme() falls back to a component-local Dark context
  when no root provided one (the use_breakpoint resilience pattern).
- Shell chrome migrated to the palette: AtTitleBar, AtTabBar,
  AtDocumentTab, AtStatusBar re-color live on toggle.
- AtTabBar gains a self-contained Dark/Light toggle button (rendered
  only when the app passes the new theme_toggle_aria_label prop -
  wired in all three apps with a new shell.ftl key; 44px touch target).
- title_bar.rs also gains the inline-editable title (TODO(title-edit)):
  an optional on_rename prop swaps the center span for a draft-owning
  input (Enter/blur commits, Escape resets; keyed by the committed
  title so external renames reseed) - read-only behavior unchanged
  when the prop is absent.

Tests: palette drift-guard, chrome-token divergence, shared-token,
well-formedness; ThemeVariant palette/toggle unit tests (extracted to
theme_tests.rs).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…vider, platform detect, font lock (4c.3/4c.4)

Built on the light-theme commit; together these close every 4c item
except the two deliberately-gated ones (router->tab navigation is an
architectural change; SVG icons wait on Blitz SVG support).

- Template browser (TODO(browse-templates)): new AtTemplateBrowser
  overlay component (AtConfirmDialog mounting contract, scrollable
  44px rows, theme-palette colors, i18n-agnostic props). loki-text's
  Browse... card is now live: a TemplateBrowserHost boundary component
  (ADR-0013) in home_util.rs mounts the dialog and forwards the
  selection to the same handler the gallery cards use. home.rs held
  at its 331-line baseline; 2 new home.ftl keys.
- Blank-doc (TODO(tabs), separable half): the tab strip's + button
  opens a blank document directly (push_new_tab(new_blank_tab()))
  instead of navigating Home. The router->tab-driven navigation half
  stays deferred as an architectural change.
- Ribbon divider (TODO(ribbon)): AtRibbonGroup gains show_divider
  (default true, so existing call sites are unchanged); AtRibbonGroups
  suppresses the trailing divider on the last in-strip group and for
  groups hosted in the vertical overflow menu.
- Platform (TODO(platform)): Platform::detect()/from_os_name resolve
  the real host OS (std::env::consts::OS), unit-tested; apps can pass
  the detected variant to platform-sensitive chrome.
- Fonts (TODO(font)): launch-time registration was already wired
  everywhere; now regression-locked by ui_font_registration.rs
  (all seven bundled families resolve in a Parley FontContext) and
  the two stale TODO notes retired.

CLAUDE.md's theme section and the deferred-features plan are updated
to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Every mounted page tile owned its own FontDataCache, and get_or_insert
clones the full face bytes per entry - so a face used on M tiles was
duplicated M times (a few MB to ~10MB of steady-state waste, bounded
only by viewport virtualization). DocPageSource - the Arc-shared object
that outlives all tiles - already held a Mutex<FontDataCache> for the
standalone render path; the GPU tile path now locks that same cache
instead of carrying a per-tile copy (tiles render serially on the frame
loop, so the lock is uncontended; poison recovery matches the file's
other locks).

New BM-9 guard bench font_cache_dedup: 8 simulated tiles inserting a
1MB face allocate 8.0MB under per-tile caches vs 1.0MB under the
shared cache; the assertion trips if the shared path ever re-clones
per tile. page_paint_source.rs held at its 314-line baseline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…emory F3 / BM-8)

A stashed tab session retained its Arc<PaginatedLayout> - Parley
layouts + byte-index maps for every paragraph, megabytes per inactive
tab, scaling with open-tab count. DocSession now keeps only the model
(CRDT, Arc<Document>, cursor/undo/save state), matching the pattern the
spreadsheet and presentation session maps already use.

Restore recomputes the layout on the same worker thread as the open
path (spawn_restore_relayout in editor_layout_task.rs): the canvas
shows the fresh-open loading indicator (total_pages = 0) for the
~tens-of-ms relayout window, then publish_restore_layout lands the
layout under two guards - the path must still be the restored tab
(mid-relayout tab switch discards) and the generation must be untouched
(an edit racing the worker relaid out already; its layout wins and the
stale restore is dropped). The publish leaves state.document alone, so
no extra Document clone is paid.

Tests: publish-lands / yields-to-racing-edit unit tests. New BM-8 bench
session_layout_residual quantifies the win: 6 stashed 120-para tabs
hold 22.7MB with retained layouts vs 5.2MB model-only - about 2.9MB
saved per inactive tab - with a 2x tripwire assertion against the
layout creeping back into the stash.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Import: docx/reader/settings.rs parses <w:mirrorMargins/> into
DocxSettings; the mapper surfaces it as the new
DocumentSettings.mirror_margins (serde(default), so old JSON snapshots
and the Loro settings bridge round-trip it for free).

Layout: LayoutOptions.mirror_margins (OR-ed in from document settings by
effective_options); finish_page swaps left/right margins on even
(verso) pages via paginate_blanks::mirrored_margins, so the inside
margin faces the binding on both pages of a spread. LayoutPage already
carries per-page margins consumed by the painter, so the swap is
complete at the flow layer. Parity is section-local page numbering
(matches document parity when sections start on odd pages).

Export: write/settings.rs emits <w:mirrorMargins/> alongside
evenAndOddHeaders; AuxParts wires the settings part whenever either
flag is set.

Tests: mirrored_margins_swap_on_even_pages_only (unit),
mirror_margins_swaps_even_page_margins_end_to_end (flow),
mirror_margins_round_trips (DOCX export->import), plus the Loro bridge
preservation assert. Frozen-baseline files (flow.rs, layout lib.rs,
mapper/document.rs) held via line-neutral comment trims; ceiling gate
green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Carry: CharProps.language (OOXML w:lang / ODF fo:language+fo:country,
already imported by both mappers) now reaches layout as the new
StyleSpan.language (Arc<str>); the paragraph cache key covers it
automatically via the Debug-fold invariant (regression-locked by a new
para_cache test).

Route: SpellState gains a per-language checker map keyed by normalized
BCP-47 tag, resolved through loki_spell::locale::fallback_chain
(SpellState::checker_for — en-GB finds an 'en' checker). Squiggle
emission (para_underlays) segments each paragraph into contiguous
same-checker ranges (merged by Arc identity so en-US|en-GB stay one
segment and words tokenize intact across the boundary) and checks each
with its own dictionary. A run tagged with a language no checker covers
is skipped rather than checked against the wrong dictionary — foreign
text stops drowning in false squiggles (LibreOffice behaviour). An
empty map preserves the legacy check-everything mode exactly.

Wire: SpellSnapshot carries the active dictionary's tag; loki-text
registers the active checker under it in both ambient-state sites, so
the skip behaviour works out of the box with the single loaded
dictionary.

Tests: mixed EN/FR routing + fallback-chain end-to-end, legacy-mode
regression, checker_for unit tests, cache-key participation. Frozen
files para.rs/resolve.rs held at baseline via comment condensing;
clippy/fmt/ceiling gates green across the four touched crates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Root cause first: production paginated layout (layout_paginated_full)
flows every section group through flow_section_group, which never
routed through the column-balancing path — balancing was only reachable
via direct flow_section calls, i.e. the tests. flow_section_group now
delegates single-section groups to flow_section (extracted to the new
flow_group.rs; flow.rs baseline ratcheted 1202 -> 1187), so real
documents finally get the existing single-page balancing.

Multi-page tail: clean-top PageStart checkpoints almost never exist for
pages after the first (page breaks happen mid-flow_block via
break_column, so the loop-top clean-page check rarely fires — verified
empirically). The block loop now records a cheap *tail candidate*
(multi-column flows only): the newest block observed to start a fresh
page plus its pre-block resume snapshot, page number corrected for the
flush observed during the block; a clean page top supersedes the guess.
flow_balance replays the tail uncapped and only balances when the
replay reproduces the natural last page (same item + glyph-run counts)
— a last page starting mid-paragraph fails this and keeps fill-first —
then binary-searches the cap and splices the balanced page over the
natural last page. Earlier pages, checkpoints, and warnings are
untouched; continuous group tails and footnote-bearing sections keep
fill-first (documented).

Tests: multi_page_two_column_section_balances_only_the_last_page,
single_section_group_routes_through_column_balancing (the production
routing regression), last_page_starting_mid_paragraph_keeps_fill_first;
full loki-layout / loki-acid (page-count canaries) / loki-text /
loki-renderer suites green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…import (5.10)

Export: the document defaults were silently dropped on ODT export — the
catalog carries them as synthetic __-prefixed styles (__Default /
__DocDefault paragraph, __DefaultChar / __DocDefaultChar character),
which the named-style writer deliberately skips, and nothing else wrote
them. A re-opened exported document therefore lost its base font, size,
and paragraph defaults. New odt/write/default_style.rs emits
<style:default-style style:family="paragraph"> (from the catalog's
is_default style) and family="text" (from default_character_style) as
the first children of <office:styles>, reusing the existing property
emitters.

Import: the ODT mapper built __Default with is_default=true but never
pointed catalog.default_paragraph_style at it (the registry's noted
follow-up), so unstyled paragraphs in ODT documents did not resolve the
document base font through effective_paragraph_style. Wired now — the
ODF analogue of the OOXML w:default="1" wiring.

meta_xml moved to the new write/meta.rs to keep write/styles.rs under
the 300-line ceiling.

Tested by default_styles_round_trip (export -> import: both families'
properties + the catalog wiring); full loki-odf suite incl. schema
validation green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Clippy -D warnings failure introduced in the previous commit — escape
is only used by write/meta.rs now.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
add_word was in-memory only: a word added to the personal dictionary
was forgotten on app restart and silently dropped when switching the
active language (activate_language parses a fresh checker with empty
overrides).

loki-spell: SpellChecker::personal_words() exposes the personal list
(lowercased, sorted, session ignores excluded) so hosts can persist and
replay it.

loki-app-shell: new spell/personal_dict.rs stores the list as a JSON
array at <data dir>/AppThere/Loki/personal-dictionary.json (Android-
aware via the existing data_root; load-or-empty + silent-write-failure,
the recent_documents pattern). SpellService::bootstrap replays the
persisted words into the fresh checker, add_word saves the full list,
and activate_language replays the in-memory list into the newly parsed
checker — memory is the truth so an add whose save failed still
survives the switch. The path is injectable
(bootstrap_with_personal_dict), so tests use scratch files / None and
never touch the user profile; the pre-existing add/ignore test now uses
the hermetic constructor (a persisted 'zzqq' would have broken its
pre-add assertion on rerun).

Tests: personal_word_survives_activate_language,
personal_word_survives_a_restart, 3 personal_dict unit tests,
personal_words_lists_added_words_sorted_without_ignores.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Root enabler: both XML trees dropped attributes — the converter's
XmlNode never captured them (so m:val characters on m:begChr/m:chr
could not be read) and the layout MNode parser skipped them (so
mfenced's own open/close were ignored and every fence rendered as
parentheses). Both now retain (local name, unescaped value) pairs.

Converter (docx/omml, new read_structs.rs / write_structs.rs; each
mapping is its own inverse, locked by assert_stable round-trips):
- m:d <-> <mfenced open/close/separators>; OMML defaults ( ) | are
  resolved on read so fence characters always travel explicitly.
- m:nary <-> munderover/munder/mover/msubsup/msub/msup with the n-ary
  <mo> as base, honouring m:limLoc, m:subHide/m:supHide, and the
  integral default; the m:e operand travels as the following sibling
  and is re-consumed by a write-side sequence lookahead keyed on the
  big-operator character set.
- m:m / m:mr <-> mtable / mtr / mtd.
- m:acc <-> <mover accent="true"> (default U+0302 circumflex).
- m:limLow / m:limUpp <-> munder / mover; a generic munderover nests
  them best-effort (stable from the second pass on).

Typesetter (loki-layout math, new stacks.rs): mfenced honours its
fence/separator attributes; munder/mover/munderover stack limits at
script size centred above/below the base (accents hug with a minimal
gap); mtable lays out a grid with per-column widths and per-row
baselines, centred on the math axis. mod.rs held under the ceiling.

Tests: 7 converter round-trips (default + custom delimiters, sum with
limits + operand, integral subSup with hidden limit, 2x2 matrix,
accent, limLow) and 4 typesetter structural tests; full loki-ooxml /
loki-layout / loki-acid / loki-text / loki-odf suites green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
The hardening pass was committed 2026-07-11 but the plan row was never
updated; no code change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…B-9)

B-8: the shared crate now owns the corpus layer, with loki-acid as its
first consumer instead of the Text-coupled home:

- appthere_conformance::corpus holds the promoted TC-* catalog (the
  four per-format tables moved wholesale; by_feature /
  cases_with_severity add the feature dimension), the Format/Severity
  vocabulary, and the new Fixture / Consumer traits — a consumer
  supplies document bytes + FixtureMeta and an import/export pair, and
  drives all three axes; the crate keeps zero Text-specific
  assumptions.
- Golden/candidate discovery is generalised into golden::discovery
  (roots as parameters) — the SSIM/dE differ was promoted earlier.
- loki-acid: catalog/severity become re-export shims (API unchanged),
  the acid Fixture enum implements the shared trait (axes derived from
  importer availability), AcidConsumer wraps the import dispatch —
  export honestly reports the corpus is import-only — and golden
  discovery delegates to the shared functions. Fixture::import and the
  consumer share one import_bytes dispatch.

B-9: the on-disk corpus is formalised as fixtures/<format>/<id>.<ext>
with goldens/<format>/<id>/, described by a code-as-manifest
(corpus::manifest::MANIFEST) recording per fixture: the feature it
exercises, which axes apply, the reference app + version behind its
golden (LibreOffice 24.2, per CALIBRATION.md), and any tolerance
override with justification (none needed since the kerning fix).
Manifest tests assert on-disk existence and that visual entries carry
a reference.

Count correction found en route: the spec's '141 TC cases' headline
miscounts — the per-format tables sum to 139, now asserted directly.
ODP/ODG/PPTX importers stay gated on the unbuilt ACID PPTX generator
(ratified §5.1 deferral).

Tests: corpus/catalog/manifest units + the loki-acid trait-wiring test
(import via Consumer, honest export failure, visual-only ODP axes);
workspace clippy/fmt/ceiling green; broad workspace test sweep green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Compact-width (< 600 px) reading now renders the reflow view's type
12.5% larger instead of being a shrunk Expanded — the last deferred
piece of Spec 03 M4 (the bounded+centred measure landed earlier).

Implemented as a pure view transform, resolving the fidelity concern
that deferred it: the layout engine runs at width / scale and tiles
paint at zoom = scale, so the on-screen tile width is unchanged, the
reading measure narrows toward the comfortable ch-range, and the
document's own point sizes are never touched.

Single source (Spec 01 A-1): the new render_layout::{
reflow_type_scale, reflow_layout_tile_width_pt,
reflow_layout_content_width_pt} feed all three consumers — paint
(document_view now sets reflow zoom to the type scale instead of 1.0),
pointer hit-testing (reflow_hit_test_window gains a type_scale param
dividing the px->pt mapping), and keyboard navigation (keydown builds
its layout at the same divided width, so the ContinuousLayout cache
stays coherent across all paths). An unmeasured (<= 1 px) viewport
keeps scale 1.0 so the first frame does not flash scaled type.

loki-renderer deliberately does not depend on appthere-ui, so its
600 px threshold is a duplicated constant drift-locked by a loki-text
test against tokens::layout::BREAKPOINT_COMPACT_MAX_PX. The reflow
width/scale metrics moved to a new reflow_metrics.rs (re-exported from
render_layout) to hold the 300-line ceiling. The Android-CPU HTML
fallback keeps its fixed 16 px body (low-fidelity path, already
comfortable).

Tests: type-scale boundaries (599.9 vs 600), CSS-tile-width invariance
across the breakpoint, narrowed Compact measure, scaled hit-test
resolving to the same paragraph, and the threshold drift lock; full
loki-renderer + loki-text suites, workspace clippy, ceiling gate green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
The tab strip rendered at 36 px at every breakpoint — below the WCAG
2.5.8 44 px minimum the strip's own doc comment flagged. It is now
breakpoint-aware: the touch-first Compact class gets TOUCH_MIN-tall
tabs and collapse toggle via the new pure tab_strip_height helper;
pointer-first classes keep the 36 px desktop density (the shell
tab-bar convention). Resilient without a responsive context (reads
Expanded), per the established use_breakpoint contract.

R-13e (condensed select sizing) was verified already built by the M3
collapse cascade: AtRibbonSelect shrinks via the ambient GroupCollapse
signal. RB-11 (bottom-ribbon-for-touch) stays deliberately unbuilt —
the audit marks it optional and the collapse cascade covers Compact.
4a.5 is complete.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
DOCX: w:tcBorders was parsed on import all along but never written —
a bordered cell exported borderless. write_table_cell now emits the
four edges (style -> w:val incl. the nil explicit-none, width -> w:sz
in eighth-points clamped to the schema's 2..=96, colour -> hex/auto)
and the tcPr children are reordered to the CT_TcPr schema sequence
(tcBorders, shd, tcMar, vAlign — shading was previously emitted after
vAlign, out of order).

ODT: the per-cell automatic table-cell style carried only the resolved
background; it now also bakes fo:border-* (reusing the paragraph
writer's ODF shorthand formatter) and fo:padding-* from the cell's
direct props. Import already read both on each side, so this closes
the round-trip.

Tests: cell_borders_and_padding_round_trip (DOCX) and
cell_borders_and_padding_round_trip_via_cell_style (ODT), each
asserting style/width/colour and that a plain neighbour stays clean;
full loki-ooxml + loki-odf suites (incl. schema validation over the
reordered tcPr) green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
ODF table default: style:default-style family="table" now maps to a
synthetic __DefaultTable catalog entry with
catalog.default_table_style pointing at it — the table family's
Default source (ADR-0012 Decision 1), closing the gap noted when the
table resolver landed (OdfDefaultStyle previously discarded the parsed
table properties).

w:cnfStyle (ECMA-376 17.4.7): Word stamps each cell with a 12-flag
mask naming exactly which table-style regions apply. New
TableCnf codec (doc-model style/table_cnf.rs) decodes/encodes the
mask; it rides the cell attr under "cnf" (the tbllook opaque-code
idiom, so it round-trips through the Loro CRDT for free). The DOCX
reader parses it, the mapper stamps the cell (validated masks only),
and the writer re-emits it as the first CT_TcPr child. Resolution:
resolve_cell_shading_cnf walks the same precedence array with
membership taken from the mask instead of positional derivation —
authoritative when present (survives row reordering and unusual band
sizes), with positional fallback for absent/malformed masks — wired
into both the layout paint (cell_style_shading_cnf) and the ODT
export's baked per-cell shading, so the two stay in agreement.

Tests: mask codec round-trip + malformed rejection, cnf-driven region
resolution precedence, mask-beats-position (a row-1 cell stamped
firstRow takes header shading; malformed falls back), DOCX cnf
round-trip, ODF table-default mapper test. Four-crate suites, clippy,
fmt, ceiling gates green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Model: TableConditionalFormat gains char_props (w:tblStylePr/w:rPr —
the bold header row of every built-in banded style; serde(default) for
pre-4a.3 catalogs). New resolvers resolve_cell_char_props(_cnf) merge
applicable regions low-to-high precedence per property (wholeTable
under bands under firstRow), with membership from the position+tblLook
derivation or an explicit w:cnfStyle mask.

OOXML: the styles reader routes a region's rPr to the region — it
previously CLOBBERED the style-level rpr (latent bug, masked only
because table-style rpr went unused); the mapper keeps regions with
shading or char props (unshaded rPr-only regions were dropped); the
writer emits the region rPr back (CT_TblStylePr order), reusing the
symmetric run-props writer.

Layout: a per-table character grid (new flow_table_chars.rs; None for
plain tables, so they pay nothing) feeds BOTH the row-height
measurement and content-flow passes — bold/large header text is wider
and taller, so the two must agree. FlowState.cell_char_defaults
carries the current cell's defaults into
resolve::flatten_paragraph_with_base, which slots the region under the
style chain per Word precedence (docDefaults < region < named styles <
direct; an unstyled paragraph's chain IS docDefaults, so the region
merges over it — the dominant table-cell case).

flow_table was extracted to flow_table_main.rs and the stale Session-4
audit narrative left resolve.rs; flow.rs ratchets 1187 -> 1101,
resolve.rs 858 -> 857.

Tests: precedence merge (firstRow size beats wholeTable, italic falls
through), end-to-end flow (header glyphs at the region's 20pt, body at
12pt), DOCX region-rPr round-trip; five-crate suites + clippy + fmt +
ceiling gates green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Doc-only: the previous commit's plan update failed to apply (stale
match); no code change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
claude added 26 commits July 12, 2026 11:08
….1, 328 → 198)

Moves the run-level intermediate-model types out of paragraph.rs into a
sibling submodule, driving paragraph.rs under the 300-line ceiling (it
leaves the baseline):

- paragraph_run.rs (run submodule): DocxRun, DocxRunChild, DocxRPr,
  DocxRFonts, DocxHyperlink, DocxDrawing (ECMA-376 §17.3.2).

The submodule imports DocxMarkRevision via its absolute path; all six types
are re-exported from paragraph.rs so every
crate::docx::model::paragraph::Docx... path is unchanged. The paragraph-
level types (DocxParagraph, DocxPPr, DocxInd, borders, tabs, etc.) stay.

Pure code motion. loki-ooxml suite green (262 lib tests + integration
binaries incl. DOCX round-trips), clippy -D warnings clean; file-ceiling
baseline shrinks 23 -> 22 (paragraph.rs 328 -> 198, paragraph_run.rs 143
under the ceiling).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…h (7.1, 331 → 210)

Moves the mutation -> relayout -> publish path out of state.rs into a
sibling submodule, driving state.rs under the 300-line ceiling (it leaves
the baseline):

- state_apply.rs (apply submodule): apply_mutation_and_relayout
  (re-derives the Document from Loro incrementally, relays out reusing
  unchanged pages, publishes state + bumps the generation) and its
  throttled log_memory_counters instrumentation.

The submodule reaches back for super::DocumentState and imports
crate::editing::relayout::{page_metrics, relayout_paginated} directly.
apply_mutation_and_relayout is re-exported from state.rs so
crate::editing::state::apply_mutation_and_relayout is unchanged for its
many callers.

Pure code motion. loki-text suite green (207 lib tests + integration
binaries), clippy -D warnings clean; file-ceiling baseline shrinks 22 -> 21
(state.rs 331 -> 210, state_apply.rs 140 under the ceiling).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…ta (7.1, 331 → 284)

Moves the Home screen's static data out of home.rs into a sibling submodule,
driving home.rs under the 300-line ceiling (it leaves the baseline):

- home_templates.rs (templates submodule): MIME_TYPES (the file-picker
  accept list) and make_templates (the localized builtin-template gallery
  cards).

The submodule imports only appthere_ui::BuiltinTemplate + loki_i18n::fl;
both items are re-imported into home.rs. The Home component and its file-
picker / routing wiring stay.

Pure code motion. loki-text suite green (207 lib tests + integration
binaries), clippy -D warnings clean; file-ceiling baseline shrinks 21 -> 20
(home.rs 331 -> 284, home_templates.rs 60 under the ceiling).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
… (7.1, 306 → 226)

Moves the two-pass w:vMerge span resolver out of table.rs into a sibling
submodule, driving table.rs under the 300-line ceiling (it leaves the
baseline):

- table_vmerge.rs (vmerge submodule): compute_v_merge_spans — assigns
  row_span to restart cells and marks continuation cells for omission
  (OOXML §17.4.84).

Re-imported into table.rs; DocxVMerge threaded into table_tests.rs (which
exercises compute_v_merge_spans directly). Four now-unused imports dropped.

Pure code motion. loki-ooxml suite green (262 lib tests + integration
binaries incl. DOCX round-trips), clippy -D warnings clean; file-ceiling
baseline shrinks 20 -> 19 (table.rs 306 -> 226, table_vmerge.rs 91 under
the ceiling).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…7.1, 312 → 138)

Moves the ODF style property-value model types out of styles.rs into a
sibling submodule, driving styles.rs under the 300-line ceiling (it leaves
the baseline):

- styles_props.rs (props submodule): OdfParaProps, OdfDropCap, OdfTabStop,
  OdfTextProps, OdfCellProps (raw-attribute-string value bags for lossless
  round-tripping).

The submodule is fully self-contained (no imports); all five types are
re-exported from styles.rs so every crate::odt::model::styles::Odf... path
is unchanged. The aggregate / style / family types stay.

Pure code motion. loki-odf suite green (245 lib tests + integration
binaries incl. ODT round-trips), clippy -D warnings clean; file-ceiling
baseline shrinks 19 -> 18 (styles.rs 312 -> 138, styles_props.rs 186 under
the ceiling).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…ers (7.1, 325 → 296)

Moves the reuse-boundary helpers out of incremental.rs into a sibling
submodule, driving incremental.rs under the 300-line ceiling (it leaves the
baseline):

- incremental_diff.rs (diff submodule): common_prefix_len / common_suffix_len
  / blocks_equal_from (block-list prefix/suffix diff that locates the changed
  range) and section_page_start (checkpoint -> starting-page lookup).

The submodule imports Block + super::PageStart; all four are re-imported into
incremental.rs. The now-unused Block import dropped from incremental.rs.

Pure code motion. loki-layout suite green (278 lib tests + integration
binaries incl. the incremental == full property test), clippy -D warnings
clean; file-ceiling baseline shrinks 18 -> 17 (incremental.rs 325 -> 296,
incremental_diff.rs 42 under the ceiling).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…ts (7.1, 324 → 82)

Moves the top-level layout entry points out of the crate root into a module,
driving lib.rs under the 300-line ceiling (it leaves the baseline):

- layout_entry.rs: layout_document (paginated/continuous dispatch),
  layout_paginated_full (paginated + the incremental-reuse checkpoints), and
  effective_options (folds document settings into the caller's
  LayoutOptions).

The module uses explicit crate:: imports; layout_document and
layout_paginated_full are re-exported from the crate root so the public API
is unchanged. lib.rs is now just module wiring + re-exports + two pub consts.

Pure code motion. loki-layout suite green (278 lib tests + integration
binaries), clippy -D warnings clean; file-ceiling baseline shrinks 17 -> 16
(lib.rs 324 -> 82, layout_entry.rs 262 under the ceiling).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…a helpers (7.1, 316 → 237)

Moves the style-picker data helpers out of editor_style.rs into a sibling
submodule, driving editor_style.rs under the 300-line ceiling (it leaves
the baseline):

- editor_style_data.rs (data submodule): collect_style_names (enumerates
  the document's (display_name, style_key) pairs: built-in headings +
  catalog styles) and style_preview_font (per-chip preview font).

collect_style_names is re-exported (pub); style_preview_font re-imported.
The style_picker_panel render fn and PICKER_HEIGHT_PX const stay; the
now-unused StyleId import dropped.

Pure code motion. loki-text suite green (207 lib tests + integration
binaries), clippy -D warnings clean; file-ceiling baseline shrinks 16 -> 15
(editor_style.rs 316 -> 237, editor_style_data.rs 98 under the ceiling).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…→ 384)

Moves the positioned-item painting cluster out of scene.rs into a sibling
submodule:

- scene_items.rs (items submodule): paint_items (walks a [PositionedItem]
  slice, dispatching each variant to the per-kind painters
  crate::glyph/rect/image/decor and recursing into clipped/rotated groups),
  paint_link_hint (hyperlink underlay), and translate_item (leaf shift).

The cluster references no other scene.rs item (fully self-contained).
paint_items is re-exported pub(crate) (also used by band.rs); the extracted
test module's #[cfg(test)] gate and translate_item import were re-threaded.

Pure code motion. loki-vello suite green (22 lib tests + integration
binaries), clippy -D warnings clean; file-ceiling baseline ratcheted
(scene.rs 613 -> 384, scene_items.rs 244 under the ceiling).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…ter (7.1, 384 → 263)

Moves paint_single_page out of scene.rs into a sibling submodule, driving
scene.rs under the 300-line ceiling (it leaves the baseline):

- scene_single_page.rs (single_page submodule): paint_single_page — draws
  one paginated page (L-shaped drop shadow, white background,
  content/header/footer/comment items, rotation-aware cursor + selection
  overlay).

It reaches back for super::{CursorPaint, the page-chrome consts,
page_chrome_size, paint_items}; re-exported pub from scene.rs (called by
paint_layout + externally).

Pure code motion. loki-vello suite green (22 lib tests + integration
binaries), clippy -D warnings clean; file-ceiling baseline shrinks 15 -> 14
(scene.rs 384 -> 263, scene_single_page.rs 140 under the ceiling).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…→ 277)

The file was one 183-line LokiPageSource::render method over a 314-line file.
Two self-contained render phases move into a sibling submodule:

- page_paint_render.rs (render submodule): allocate_page_texture (GPU
  texture + view allocation) and page_cursor_paint (page-relative cursor
  paint data, layout guard scoped internally).

Both take only render-loop-passed data — no LokiPageSource state beyond the
source handle. The now-unused anyrender_vello::wgpu import list moved with
them.

Pure code motion. loki-renderer suite green (18 lib tests + integration
binaries), clippy -D warnings clean; file-ceiling baseline shrinks 14 -> 13
(page_paint_source.rs 314 -> 277, page_paint_render.rs 81 under the ceiling).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Extracts the mutually-recursive body-child dispatcher into a sibling submodule:

- document_body.rs (body submodule): read_section (text:section) and
  read_body_children (the shared body-children loop dispatching
  text:p/h/list/table/table-of-content/section/tracked-changes/indexes).

It reaches the parent's leaf readers via super::{read_list, read_paragraph,
read_table, read_toc, skip_element} and calls read_tracked_changes by its
full crate path. read_body_children is re-exported into the parent (used by
read_document); the now-unused OdfSection import was dropped from document.rs.

Pure code motion. loki-odf suite green (245 lib tests + integration
binaries), clippy -D warnings clean; file-ceiling baseline shrinks 13 -> 12
(document.rs 367 -> 276, document_body.rs 111 under the ceiling).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Extracts the shared parse-and-map pipeline into a sibling submodule:

- import_package.rs (package submodule): parse_and_map_package (~160-line
  workhorse — opens the OPC package, locates officeDocument, parses
  body/styles/numbering/notes/settings/comments, collects
  hyperlink/image/header/footer related parts, calls map_document) plus its
  four OPC part/rels resolution helpers (map_xml_err, resolve_part_name,
  rels_by_type, resolve_optional_part + the OpcResult alias) and the
  resolve_part_name tests.

parse_and_map_package is re-exported pub(crate) from import.rs (used by
DocxImporter::run and crate::docx::mapper). The two helpers still needed by
the sibling import_pic_bullets were promoted pub(super) and its super::
calls repointed at super::package::. The twelve now-unused imports were
dropped from import.rs.

Pure code motion. loki-ooxml suite green (262 lib tests + integration
binaries), clippy -D warnings clean; file-ceiling baseline shrinks 12 -> 11
(import.rs 387 -> 129, import_package.rs 279 under the ceiling).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Extracts the ZIP-entry reading helper cluster into a sibling submodule:

- package_read.rs (read submodule): validate_mimetype, read_entry (capped
  read + UTF-16->UTF-8 transcode), collect_images (Pictures/), collect_objects
  (embedded content.xml sub-docs), plus the internal transcode_utf16_to_utf8 /
  infer_media_type and the four infer_media_type tests.

The four impl-facing entry points are promoted pub(super) and re-imported
into package.rs. The XML local_name_bytes helper stays with the struct/impl.
Now-unused imports (CompressionMethod, read_entry_capped, five mimetype
constants) were dropped from package.rs (the MIME_ODT doc-link repointed at
its full path); the parent test file gained explicit ENTRY_MIMETYPE/MIME_ODT
imports.

Pure code motion. loki-odf suite green (245 lib tests + integration
binaries), clippy -D warnings clean; file-ceiling baseline shrinks 11 -> 10
(package.rs 410 -> 223, package_read.rs 233 under the ceiling).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…→ 249)

Extracts two cohesive element-parsing clusters into sibling submodules:

- document_ppr.rs (ppr submodule): the w:pPr cluster — parse_ppr_element +
  apply_ppr_attr + parse_pbdr + parse_tabs. parse_ppr_element is re-exported
  pub(crate) (external callers in styles.rs / numbering.rs).
- document_drawing.rs (drawing submodule): the w:drawing cluster —
  parse_drawing + parse_wrap_text. parse_drawing is re-exported pub(crate)
  (called by the run submodule).

Both reach shared readers via crate::docx::reader::{util,sectpr} and
super::parse_rpr_element. Fourteen now-unused imports (nine Docx* paragraph
-prop types, parse_emu/toggle_prop, FloatWrap/TextWrap/WrapSide) were dropped
from document.rs.

Pure code motion. loki-ooxml suite green (262 lib tests + integration
binaries), clippy -D warnings clean; file-ceiling baseline shrinks 10 -> 9
(document.rs 540 -> 249, document_ppr.rs 233 / document_drawing.rs 93 under
the ceiling).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
… 229)

Extracts two cohesive writer clusters into sibling submodules:

- document_para.rs (para submodule): the paragraph writers — write_para,
  write_styled_para, and the w:pPr child emitter write_para_props_inline.
  write_para / write_styled_para are re-exported for the write_block
  dispatcher and the blocks submodule.
- document_blocks.rs (blocks submodule): the non-paragraph block writers —
  write_code_block, write_horizontal_rule, write_line_block, write_list_item.
  All are re-exported; the recursive write_list_item reaches back for
  super::{write_block, write_para}.

RunProps (referenced externally by fields.rs / document_inlines.rs) stays in
document.rs; child modules construct it via field-privacy-through-descendants.
Shared writers are reached via crate::docx::write::{xml,run_props,revision}
and super::inlines; the super::revision::write_mark_del call was repointed at
its full crate path; six now-unused imports were dropped.

Pure code motion. loki-ooxml suite green (262 lib tests + integration
binaries), clippy -D warnings clean; file-ceiling baseline shrinks 9 -> 8
(document.rs 540 -> 229, document_para.rs 227 / document_blocks.rs 125 under
the ceiling).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Extracts three cohesive units out of the styles reader:

- styles_list_style.rs (list_style submodule): parse_list_style — the
  text:list-style reader dispatching bullet/number/image/none levels via the
  existing list submodule. Re-exported for read_stylesheet.
- styles_auto.rs (auto submodule): read_auto_styles (the content.xml
  office:automatic-styles fast reader) + parse_style_family. read_auto_styles
  re-exported pub(crate) for odt/import.rs; parse_style_family re-exported for
  read_stylesheet.
- parse_text_props_attrs (the style:text-properties attribute reader) moved
  into the existing props submodule (cohesive with parse_style_props) and
  re-exported so the list/props submodules' super::parse_text_props_attrs
  paths keep working.

Six now-unused type imports were trimmed from styles.rs; the extracted test
file gained explicit OdfStyleFamily/OdfListLevelKind imports.

Pure code motion. loki-odf suite green (245 lib tests + integration
binaries), clippy -D warnings clean; file-ceiling baseline shrinks 8 -> 7
(styles.rs 599 -> 295; styles_list_style.rs 182 / styles_auto.rs 128 /
styles_props.rs 217 under the ceiling).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Extracts two cohesive placement clusters into sibling submodules of para_impl:

- flow_para_place.rs (place submodule): place_paragraph_layout — continuous
  vs paginated placement, keep-together, and line-boundary splitting.
  Re-exported for flow_paragraph and the chain cluster.
- flow_para_chain.rs (chain submodule): the keep-with-next chain cluster —
  flow_keep_with_next_chain + build_chain_layouts + place_chain_blocks +
  place_chain_too_tall + the CHAIN_LIMIT const. flow_keep_with_next_chain is
  pub(crate), re-exported pub(super) from para_impl for flow.rs.

Both child modules reach the flow-state helpers (break_column, finish_page,
push_editing_para, split_and_place_loop, FlowState, LayoutWarning) through
para_impl's existing imports via super::, and the block synthesizers via
super::super:: (the flow module). place_paragraph_layout is shared between
flow_paragraph and the chain via the place re-export. Two now-unused imports
(Arc, Block) were dropped from flow_para.rs.

Pure code motion. loki-layout suite green (278 lib tests + integration
binaries), clippy -D warnings clean; file-ceiling baseline shrinks 7 -> 6
(flow_para.rs 522 -> 236; flow_para_place.rs 96 / flow_para_chain.rs 225
under the ceiling).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Two-file split of the inline-flattening machinery (the walk_inlines monolith):

- resolve_walk.rs (walk submodule): walk_inlines — the recursive inline-tree
  walker (~277 lines), pub(super).
- resolve_inlines.rs (inlines submodule): the flattening leaves —
  flatten_paragraph_with_base (entry, re-exported pub for flow_para /
  flow_para_chain), the text emitters push_text/push_small_caps/apply_link,
  field_display_text, superscript_mark, and the Inline::Image arm extracted as
  collect_inline_image.

The two submodules are mutually recursive: inlines::flatten_paragraph_with_base
and collect_inline_image call super::walk::walk_inlines; walk_inlines calls
super::inlines::{push_text, field_display_text, superscript_mark,
collect_inline_image}. Extracting the Image arm kept the walk file under the
ceiling. resolve.rs keeps the pure resolvers (resolve_color/pts_to_f32/
emu_to_pt, CollectedImage/CollectedNote, resolve_para_props/resolve_char_props,
the flatten_paragraph wrapper, convert_border). NodeAttr was imported from its
real content::attr path; eight now-unused imports were trimmed.

Pure code motion. loki-layout suite green (278 lib tests + integration
binaries), clippy -D warnings clean; file-ceiling baseline shrinks 6 -> 5
(resolve.rs 665 -> 194; resolve_walk.rs 266 / resolve_inlines.rs 266 under
the ceiling).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Extracts three cohesive clusters into sibling submodules; flow.rs keeps only
the public types (FlowOutput/LayoutWarning), the FlowState struct/impl, and
the module wiring:

- flow_run.rs (run submodule): new_flow_state + run_paginated_loop — the
  flow-state constructor and top-level paginated block loop.
- flow_entry.rs (entry submodule): flow_section + flow_section_resume +
  begin_continuous_section — the public section entry points.
- flow_dispatch.rs (dispatch submodule): flow_block + flow_blocks +
  finish_page — block dispatch and page finalization.

Every function reaches the other clusters and the fourteen existing sibling
submodules through flow.rs's imports via super:: (children see the parent's
private use-imports). flow_block/finish_page were promoted pub(crate) in
dispatch so flow.rs can re-export them pub(super) for the external submodule
callers (flow_para/flow_tail/flow_balance/flow_group/flow_table_geom);
new_flow_state/run_paginated_loop/begin_continuous_section stay reachable as
super::* for flow_balance/flow_group. Seven now-unused imports were trimmed.

Pure code motion. loki-layout suite green (278 lib tests + integration
binaries), clippy -D warnings clean; file-ceiling baseline shrinks 5 -> 4
(flow.rs 721 -> 277; flow_run.rs 171 / flow_entry.rs 175 / flow_dispatch.rs
167 under the ceiling).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…rtial, 834 → 770)

Moves the tab-stop expansion probe pass (gap #7 — lays the paragraph out with
zero-width inline boxes at each \t / decimal separator to measure x-positions,
then resolves each into a TabPlan) out of the 580-line
layout_paragraph_uncached into para_tabs.rs::measure_tab_plans — the module
that already owns compute_tab_plans / emit_tab_leader / TabPlan, colocating the
tab logic. Reaches back for super::{push_para_styles, push_math_inline_boxes,
DEC_ID_BASE, END_ID}.

Partial: para.rs 834 -> 770 (baseline cap ratcheted down but NOT cleared).
Unlike the files cleared this pass, para.rs is dominated by one cohesive
580-line algorithm whose phases share heavily-mutated state, and the natural
home for the remaining big phase (para_emit.rs) is already at the ceiling;
clearing it under 300 would require gutting the core paint path. See the
plan doc for the honest status and recommendation to keep para.rs (+ the
three Dioxus components) as deliberate baseline entries.

Pure code motion. loki-layout suite green (278 lib tests + integration
binaries), clippy -D warnings clean; baseline stays at 4 (para.rs cap
834 -> 770; para_tabs.rs 280 under the ceiling).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…low]`

Mirrors the file-ceiling ratchet for the audit's Q-3/Q-4 debt:

- scripts/check-suppressions.py counts `let _ = …` (discarded/ swallowed
  Results) and `#[allow(…)]`/`#![allow(…)]` (lint suppressions) per production
  .rs file (test files excluded, same scope as the file-ceiling gate).
- scripts/suppressions-baseline.txt freezes the current population per file:
  419 `let _ =`, 170 `#[allow]` across 134 files.
- Ratchet rules match the file ceiling: a non-baselined file must be 0/0; a
  baselined file's two counts may only shrink; a file dropping to 0/0 must
  leave the baseline; a stale entry (missing/renamed file) fails. So adding a
  genuinely-needed suppression becomes a deliberate, reviewable `--update`
  rather than silent accretion.
- Wired into .github/workflows/rust.yml right after the file-ceiling gate.

First opportunistic reduction: a strip-and-rebuild probe showed most OOXML
`#[allow(dead_code)]` are stale (removing 17 produced only 11 real dead-item
warnings). docx/model/styles.rs's five were all unnecessary — removed; clippy
-D warnings stays clean, `#[allow]` total 175 -> 170. The genuinely-dead items
keep their now-baselined allows (deleting intentional model/round-trip surface
is the big-bang the audit warns against; left for per-file follow-up).

loki-ooxml suite green (262 lib tests + integration binaries), cargo check
--workspace clean; both ratchet gates pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
The `cargo udeps` piece of the Spec 01 residuals, done as a stable-toolchain
stand-in (cargo-udeps isn't installable here): a per-crate heuristic scan
surfaced 9 candidates, each authoritatively verified by removal + cargo check.

Three genuinely-unused deps removed (each confirmed to compile without it, the
optional one under its own feature):

- loki-pdf: loki-primitives (unreferenced).
- loki-server-api: tokio — the [dependencies] `sync`-feature entry; the
  [dev-dependencies] tokio used by tests stays.
- loki-sheet-model: serde_json (+ its `dep:serde_json` entry in the `serde`
  feature) — the feature's cfg_attr derives need only `serde`, not serde_json.

The other six candidates were false positives the compile-check correctly
rejected and kept: appthere-color / loki-primitives (used via non-`::` paths),
async-trait (attribute-macro use), and fontique x3 (a deliberate
feature-enabler forcing fontconfig-dlopen across the workspace, documented in
each app manifest).

The pedantic rollout (2/43 crates today), the no_hardcoded_layout_dims dylint
(needs the nightly dylint driver; the viewport-dims Python gate stands in), and
Android-target CI (needs the NDK — `cargo check` for the target fails on a
-sys cc build here) remain toolchain-gated; see the plan doc.

Cargo.lock collapsed by one entry; cargo check --workspace clean, loki-pdf +
loki-sheet-model suites green (51 tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Adds the audit T-3 "still open" per-case P0 round-trip assertions. These use a
build -> export -> re-import -> assert-present shape rather than the divergence
helper (conformance_round_trip.rs), which compares two consecutive cycles and
is blind to loss on the FIRST export (seed->a drops a property, a->b keeps it
dropped, a==b passes falsely). New tests/conformance_p0_round_trip.rs (DOCX) +
conformance_xlsx_p0_round_trip.rs (XLSX), 5 cases — and, per the fix-the-cause
principle, the three real exporter/importer gaps they exposed are fixed:

- TC-DOCX-021 (bookmark + REF/PAGEREF cross-reference): already round-tripped;
  pure test addition.
- TC-DOCX-023 (floating-image wrap modes): the DOCX writer emitted only
  wp:inline and ignored the image's FloatWrap, so every floating image
  exported as inline and lost its wrap. Added write_anchor_drawing emitting
  w:drawing/wp:anchor + the wrap element (wrapSquare/Tight/Through/
  TopAndBottom/None + wrapText side + behindDoc), sharing the pic:pic body
  with the inline path via a new write_pic_graphic. Anchor position is
  unmodelled (reader recovers only wrap + behindDoc) -> zero offset.
- TC-DOCX-029 (RTL/bidi): w:bidi was read on import and mapped to
  ParaProps.bidi but never written on export; added the w:bidi toggle emit to
  write_para_props_inline. (ODT export already wrote style:writing-mode.)
- TC-XLSX-009/011 (dynamic-array spill + XLOOKUP/modern functions): the <f>
  text handler OVERWROTE current_formula per text event instead of appending,
  so any formula containing an XML-escaped char (&gt;/&lt;/&amp;/&quot; —
  ubiquitous in comparison formulas like FILTER(A1:A10,B1:B10>0)) kept only
  the fragment after the last entity (e.g. "0)"). Changed to
  get_or_insert_with(String::new).push_str, matching the value handler.

ODT export coverage (T-2) was already satisfied (odt/write/ ships with
odt_export_round_trip.rs + metadata_round_trip.rs). Hard PPTX cases (T-5) stay
deferred — they need a PowerPoint-authored deck that can't be produced here.

Also fixed unused-import warnings the earlier 7.1 xlsx-split left in
xlsx/import.rs + xlsx/export.rs — feature-gated, so invisible without
--all-features (which CI runs), and would have failed clippy -D warnings.

Suppression baseline (7.3): document_drawing.rs `let _ =` 35->66 (new writer
follows the file's `let _ = write_*` house idiom) while its #[allow] dropped
2->1 (renamed cx_s/cy_s, removing the similar_names allows); document_para.rs
`let _ =` 22->24 (two bidi writes). Regenerated deliberately.

Docs: fidelity-status.md bidi/RTL (export No->Yes) and Floating Images (export
No->Partial) rows updated; deferred-features-plan 7.5 row marked done.

loki-ooxml --all-features suite green (205 lib + integration incl. the 5 new
cases), workspace clippy --all-features -D warnings clean, fmt clean,
file-ceiling + suppression gates green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
The three app shells (loki-text/loki-spreadsheet/loki-presentation) carried
byte-identical (bar doc-comment wording) tab-management logic in
routes/shell.rs::close_tab and routes/home_util.rs::{push_or_switch_tab,
close_tab_for_path}. The bug-prone active-tab / navigation index arithmetic is
now single-sourced in loki_app_shell::tabs as pure, dependency-free functions:

- open_or_switch(&mut Vec<OpenTab>, path, title) -> usize
- close_by_path(&mut Vec<OpenTab>, active, path) -> usize
- resolve_tab_close(idx, current_active, len_before) -> Option<TabCloseOutcome>
  (with TabCloseNav::{Stay, Home, Editor(idx)})

Each app keeps a thin Signal wrapper that reads/writes its own signals, drops
its app-specific DocSessions entry, and applies the router nav — so no Dioxus
dependency is added to loki-app-shell and it stays headlessly buildable. The
logic is now covered by 13 unit tests (tabs_tests.rs) instead of being
copy-pasted 3x untested.

Deliberately not extracted (documented residuals):
- The Shell component + Route enum: a Dioxus #[component] can't be generic
  over a per-app Routable, and each app's Route/DocSessions/ribbon differ, so
  the component shell and route construction stay per-app.
- display_title_from_path + helpers (~90 lines x3): moving it needs
  loki-app-shell to depend on loki-file-access, which target-gates rfd ->
  wayland-sys on desktop Linux (not a Cargo feature), making the shared-logic
  crate un-buildable headlessly. Better closed upstream by gating rfd behind a
  loki-file-access feature; left as a documented residual.

Verification: installed libwayland-dev/libxkbcommon-dev to unlock the full GUI
build, then compiled + clippy-cleaned all three apps and loki-app-shell (exit
0; only the pre-existing vendored blitz-dom dependency warning); 13 new unit
tests pass; fmt clean; file-ceiling / suppression / license gates green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
The 6.5 mandate was "re-measure first, then fix if confirmed." Measured all
three; the gate did its job — none justify a change now.

P-3 (glyph-run span scans) — measured, refuted. New permanent bench
(loki-bench/benches/portable_span_density.rs) lays out a fixed 256-word
paragraph split into N distinct style spans (cache cleared each rep → cold
emit path) and microbenches the isolated find-per-run scan vs a byte→span
index map. End-to-end layout is strongly sub-linear in span count (4→256
spans = x2.4, not the ~x4000 a dominant O(N^2) implies; Parley shaping
dominates). The isolated scan is ~0.7% of layout at N=64 and ~6% at a
pathological N=256, <0.1% at realistic <20 spans/para. Decisively, the
proposed byte→span-index map is SLOWER for small N (map build out-costs a
short scan), so it would regress the common case. Also 3 of the 4 named
scanners are already gone (moved to selection-geometry underlay passes) and
the per-call range.clone() is gone. No fix; bench kept as a regression guard.

P-5 (coarse render invalidation) — confirmed coarse, fix device-gated. The
flagged page_cache.rs::mark_all_dirty no longer exists; invalidation is now
loki-renderer's generation-keyed DocPageSource (a mutation bumps the
generation → all visible tiles re-render). Bounded by viewport virtualization;
the real payoff is GPU frame-time (device axis, not headless-measurable) and
the fix is a substantial per-page render-dirty refactor. Deferred to the
device-frame-time bench rather than speculatively refactored.

P-6 (cold-path clones) — confirmed negligible. snapshot_checkpoint clones
list_counters (empty for list-free docs) per page boundary at layout time,
dwarfed by Parley allocations. Opportunistic-only, per the audit's P3 rating.

Docs: plan 6.5 row + audit P-3/P-5/P-6 entries updated with the measurements.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
@kevincarlson kevincarlson merged commit 3ea706f into main Jul 13, 2026
2 checks passed
@kevincarlson kevincarlson self-assigned this Jul 13, 2026
@AppThere AppThere locked as resolved and limited conversation to collaborators Jul 13, 2026
@kevincarlson kevincarlson deleted the claude/quality-pass-perf-cleanup-ecgj28 branch July 13, 2026 12:35
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants