Spec audit reports + conformance harness + responsive UI foundation#29
Merged
Conversation
Place Spec 01 (Codebase Audit & Target Architecture) in docs/adr and produce its first two deliverables per the spec's audit-first method: - spec-01-audit-report.md (M1): read-only inventory across every §4.1 smell category with counts and a triage table (Priority left blank for the maintainer, per D1). Includes the dedicated 1280px-class trace: the window_width signal defaults to 1280 and is never written, while the measured viewport width lives in a separate scroll_metrics.client_width used only for reflow -- so hit-test and reflow diverge off 1280px. - 0009-target-architecture.md (M2): seven-layer target derived from the real cargo-metadata graph. The UDOM waist already holds (no format crate leaks into layout/render); exactly one uphill edge exists (loki-renderer -> appthere-ui) plus two classification refinements. No production code changed. Enforcement (M3) and fixes (M4) follow maintainer triage of the Priority column. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
Auto-appended allow-list entries from this session. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
Place Spec 02 (Conformance Testing Harness) in docs/adr and produce its audit-first inventory per §3.1 -- read-only, no code changed. Key findings mapped against the existing loki-acid harness (141 TC-case corpus, SSIM differ, golden discovery already present): - Blocking (B-1): render path is vello 0.6 on wgpu (GPU); no vello_cpu and no software adapter in this GPU-free environment. D2's CPU-rasterizer path is unbuilt and is the critical-path decision for the visual axis. - Schema axis (B-6): 0 vendored schemas, but xmllint IS installed here, so the libxml2-backed validator is immediately feasible. - Round-trip (B-7): 25 ad-hoc per-crate round-trip tests exist but there is no shared normalized-model differ with first-divergence path reporting. - Visual: 0 goldens committed; threshold is a guessed SSIM 0.98 (violates D5); differ is SSIM-only/averaged (no CIEDE2000, worst-region, heatmap). - Fonts (D4): metric-compatible set (Carlito/Caladea/Tinos/Cousine/Arimo) already bundled; Gelasio (Georgia) absent. soffice/libreoffice present for ODF golden generation. appthere-conformance does not exist yet; the inventory recommends promoting loki-acid's modules into it. Triage table has Priority left blank for the maintainer (mirrors Spec 01 D1). M1-M6 implementation deferred pending triage and the B-1 decision; M6 also gated on Spec 01 CI work. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
Update the Spec 02 conformance-testing harness doc per maintainer revision: - B-1 resolved: conformance rendering uses an in-process vello_cpu software rasterizer (D2 rewritten); pinned-llvmpipe wgpu and a local-only visual gate considered and rejected. - Reframe the harness as a promotion of loki-acid's reusable modules into appthere-conformance rather than a greenfield build; milestones, working method, and the shared-crate section updated to match. - Font parity (D4 / §7.3): add Gelasio (Georgia) as a bundled asset — the one missing metric-compatible face the audit found. - Calibration (§7.4 / D5): name the hardcoded SSIM_THRESHOLD = 0.98 as the magic number to retire; note the no_hardcoded_layout_dims lint won't catch it, so calibration is the enforcement. - Status line records B-1 resolved; acceptance criteria reference xmllint / soffice availability confirmed by the inventory. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
Annotate the audit-first inventory with the maintainer's resolutions from the revised spec, preserving the original findings as the snapshot: - B-1 (CPU rasterizer): resolved -> in-process vello_cpu; llvmpipe-wgpu rejected. Now an M5 implementation task, not an open blocker. - B-10 (Gelasio/Georgia font): resolved -> bundle Gelasio. Now an asset-add. Updates the status banner, executive summary, axis/environment tables, triage table (B-1/B-10 marked Resolved), and sequencing. B-2..B-9 and B-11 remain open; Priority still the maintainer's. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
loki-opc is released as a standalone crate and is MIT-licensed; the rest of the suite is Apache-2.0. Document the distinction and enforce it per-crate. - ADR-0010: per-crate licensing policy (loki-opc MIT, suite Apache-2.0), authoritative source = each crate's Cargo.toml `license`. - scripts/check-license-headers.py: license-aware CI gate. Resolves each first-party .rs file to its owning crate and fails if line 1 is not the SPDX id matching that crate's declared license. Vendored patches/* excluded; reviewed exceptions file added. Wired into rust.yml ahead of fmt/clippy. - loki-opc: SPDX id moved from line 2 to line 1 in 27 files (copyright now line 2, matching the Apache crates), header added to tests/package_tests.rs, and a crate-local MIT LICENSE added so the standalone release is self-contained. Comment-only; `cargo check -p loki-opc` passes. - CLAUDE.md: license-header convention generalised from blanket Apache-2.0 to "line-1 SPDX matches the crate license," noting the loki-opc MIT exception. - Spec 01 audit report: correct the A-3 false positive (the scan matched only the literal Apache-2.0 string, so loki-opc's MIT headers read as absent); record the real finding (SPDX on line 2 + one missing header) as resolved. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
A-4 (dead/stray files): remove scratch.rs (a throwaway Dioxus API probe) and 10 committed debug/render dumps at the repo root (diff_flow.txt, iris*.png/log, output.png, test_output*, test_log*). Confirmed no tracked code/README references them; the render_to_png example writes its own ./output.png. Added .gitignore entries so they can't drift back in. Frees ~3.6 MB of tracked junk. A-6 (panic! in lib): add scripts/check-no-panics.py — a CI gate forbidding panic!/todo!/unimplemented! in first-party library src (test modules, tests/, benches/, examples/ excluded). Baseline green: lib src has none today. Messaged unreachable! (7 documented invariant-assertions) is intentionally permitted. Wired into rust.yml. A-9 (layering classification): already resolved by ADR-0009's L3b exporter-above-layout tier (loki-pdf consumes layout -> L3b; loki-epub is pure model -> L2 waist). Marked resolved in the audit triage table. A-10 (error handling): verified the core library crates are thiserror-clean. The only Result<_, String> holdouts are 2 in the loki-acid test harness and 1 deliberately UI-facing app-glue fn in loki-presentation (documented as returning a human-readable string) — none in core libs. Recorded as a noted residual; mechanised enforcement rides with the deferred clippy disallowed-types gate. No core-lib remediation needed. Updates the Spec 01 audit report (§3.6, §3.7, triage table, enforcement table, executive summary) to reflect the four resolutions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
…_main dup) A-14: the ~55-line android_main NativeActivity bootstrap was duplicated near-verbatim across loki-text/-presentation/-spreadsheet (only the logcat tag and the init_android argument differed). Extract it into a loki_app_shell::android_main! macro (loki-app-shell/src/android.rs); each binary now invokes it in one line. A macro (not a function) keeps the dioxus/blitz_shell/android_activity deps in the binaries, so loki-app-shell gains no dependencies and stays #![forbid(unsafe_code)] — the emitted unsafe lands in the caller. The three near-divergent copies are unified to one canonical body (presentation/spreadsheet gain one benign insets-log line). A-7: the three Android cdylib crates omitted #![forbid(unsafe_code)] entirely because #[unsafe(no_mangle)] on android_main makes forbid impossible. Each crate root is now #![deny(unsafe_code)] with the single unsafe item (the macro-generated android_main) carrying a scoped #[allow(unsafe_code)]. Add scripts/check-unsafe-policy.py + unsafe-policy-allowlist.txt: every crate root must be forbid, or deny + allow-listed, so a new unsafe crate can't appear silently. Wired into CI. Verification: cargo check of all three app crates + loki-app-shell passes and clippy is clean on app-shell; fmt + all gates green. The android_main body is #[cfg(target_os = "android")] and cannot be compiled in this GPU-less Linux environment, so the Android target build is UNVERIFIED here — maintainer should run scripts/build-aab.sh before relying on it. Updates CLAUDE.md unsafe convention and the Spec 01 audit report (A-7/A-14, enforcement table, triage). A-14's centring-math cluster stays deferred to the A-1 Viewport refactor. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
A-2: the 300-line file ceiling (CLAUDE.md) was unenforced — 38 production files exceeded it. Add scripts/check-file-ceiling.py: a ratchet gate against scripts/file-ceiling-baseline.txt. A new file must be <= 300 lines; a baselined file may not grow; a baselined file split to <= 300 must leave the list. So the backlog can only shrink and no new over-ceiling file can be born. Wired into CI. Ratchet behavior verified (new file, growth, and stale-entry cases all fail). Down-payment: the 3 files that were over-ceiling *solely* because of a large inline #[cfg(test)] module are split via the safe inline-test extraction idiom (#[path] sibling, zero production-code change): - loki-text/src/editing/hit_test.rs 561 -> 197 - loki-ooxml/src/xml_util.rs 363 -> 188 - loki-pdf/src/page.rs 322 -> 218 Verified: cargo test passes for loki-pdf and loki-ooxml; loki-text test build compiles. Baseline now holds the remaining 35 (genuinely large files stay the maintainer's directory-split backlog, but can no longer grow). Updates the Spec 01 audit report (A-2, enforcement table, triage) and the CLAUDE.md tech-debt section (gate + the 3 new extractions). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
…ming)
A-5 — library unwrap/expect. A precise clippy::unwrap_used/expect_used scan
(no false positives on the spreadsheet parser's custom expect() method) found
exactly 11 genuine first-party library sites; all rewritten, zero library
#[allow] needed:
- loki-opc (x6): core_properties_mut -> get_or_insert_with; strip_suffix
pair -> unwrap_or (+ collapse redundant if/else); PartName::new_unchecked
made infallible (-> Self; it was Ok(Self(s))), dropping 3 unwraps.
- loki-ooxml/xlsx/export.rs -> let-else continue.
- loki-layout: lib.rs -> match groups.last_mut(); para.rs -> bind band via
as_ref().filter (Band isn't Clone, so layout_band_body now borrows it).
- loki-i18n (x2) -> compile-time langid!("en-US") helper (no runtime parse).
Enforcement: clippy::unwrap_used/expect_used denied in CI; clippy.toml exempts
#[cfg(test)] (allow-*-in-tests). build.rs scripts + the gen_templates codegen
bin carry a justified file-level allow (a panic there aborts the build/tool).
A-11 — TODO/COMPAT debt. All 47 production TODOs already carry a TODO(<topic>)
tag (0 bare). Catalogued in docs/adr/spec-01-todo-compat-inventory.md (COMPATs
grouped by target; 32 are dioxus-native, to re-validate on a Dioxus-pin bump).
New scripts/check-todo-format.py gate (CI) blocks bare TODO / FIXME / HACK / XXX.
A-12 — *Props naming. Decision (ADR-0011): keep *Props for both the model
formatting bags (ParaProps/CharProps/...; mirror OOXML pPr/rPr) and Dioxus
component props. They never collide (disjoint crates/modules). The proposed
rename is 350+ refs across 66+ files touching the CRDT bridge and every format
mapper -- disproportionate to a low-priority cosmetic concern. Available on
request as its own PR.
Verified: full-workspace clippy deny is green; loki-layout (175+), loki-opc,
loki-i18n, loki-ooxml tests pass; fmt + all six structural gates green. The
file-ceiling baseline is bumped for xlsx/export.rs (+3 lines from the A-5 fix).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
…-14)
The headline audit finding. `window_width` was a Signal<f32> defaulting to
1280.0 in editor_state.rs that was NEVER written, while the real viewport width
was measured separately into scroll_metrics.client_width (via get_client_rect)
and used for reflow/render. Hit-testing/pointer/spell-panel math read the stale
1280; rendering read the real width — so click-to-caret mapping drifted on every
window that wasn't exactly 1280px wide.
Fix:
- Delete the window_width signal and its 1280 default.
- New loki-text/src/editing/viewport.rs: `Viewport { inner_width_px }` with
`centred_origin_x(content_width)`, built from the measured
scroll_metrics.client_width at each use site. Hit-test and render now derive
the canvas origin from the SAME measured value, so they can't diverge.
- The six copied `(window_width - …)/2.0` centring expressions (pointer x4,
reflow tap, spell-panel clamp) collapse to the one helper — closing A-14's
remaining "centring math" cluster (the android_main cluster was already done).
- make_mousemove/touchmove/touchend_handler and reflow_tap_position now take the
viewport/scroll_metrics instead of the window_width signal; the spell panel's
param is renamed viewport_width.
- Update the stale hit_test.rs "defaults to 1280px" doc comment.
Page geometry stays in DocumentState; zoom/DPI can join Viewport when Spec 03
(Responsive) needs them. The no_hardcoded_layout_dims dylint (A-13) remains the
future backstop against re-introducing such a literal.
Verified: cargo test -p loki-text passes (incl. 4 new Viewport unit tests);
clippy -D unwrap_used/expect_used clean on loki-text; fmt + all six gates green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
A-8 — the one uphill dependency. loki-renderer (render, L4) imported
appthere_ui (ui, L5) solely for two design-token constants (PAGE_GAP_PX,
SPACE_6) in document_view.rs. Inject them via DocumentViewProps
(page_gap_px, content_padding_bottom_px) — the app, which legitimately
depends on appthere_ui, wires them down — and drop the appthere-ui dependency
from loki-renderer/Cargo.toml. The workspace graph is now fully downhill.
Verified: cargo check -p loki-renderer and cargo test -p loki-text pass.
A-13 — enforcement gates. Two new CI gates close the architectural and
viewport-literal conventions:
- scripts/check-dependency-direction.py: parses `cargo metadata` and fails on
any uphill internal edge per ADR-0009's layer map (loki-acid exempt;
loki-templates mapped at L2). Green: 71 edges all downhill; verified it would
catch the former render→ui edge.
- scripts/check-no-hardcoded-viewport-dims.py: blocks bare screen-resolution
literals (1280, 1920, …) in the editor input/viewport hot-paths, where width
must come from the measured Viewport (A-1). Named consts are the escape
hatch; the 800.0 height fallback in editor_canvas is now the named
DEFAULT_VIEWPORT_HEIGHT_PX. This is the pragmatic stand-in for the
no_hardcoded_layout_dims dylint, which needs nightly/dylint infrastructure
absent from this stable CI (documented as deferred).
The lint job now runs nine structural gates. Updates ADR-0009 (M-1 done; graph
clean; loki-templates L2) and the Spec 01 audit report (A-8/A-9/A-13 resolved;
enforcement table; residuals: clippy::pedantic set + general dylint).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
…built) When the conformance inventory was filed it flagged that M6 (CI integration) was blocked on Spec 01's enforcement pipeline being "specified but not yet implemented". That pipeline now exists — rust.yml's lint job runs nine structural gates — so: - B-11: marked Unblocked. The schema/round-trip gates plug into the populated lint job with the same script-gate pattern as the gates already there. - §8 sequencing + §9 scope note: the "M6 gated on un-built Spec 01 infra" caveat no longer applies; updated accordingly. - Added a dated "Spec 01 enforcement update" banner alongside the B-1/B-10 one. - Minor: B-6's loki-opc SPDX reference is now resolved (ADR-0010); B-3's "1280px-class" analogy updated (Spec 01 A-1 eliminated that class). Annotations only — the audit-first snapshot and open findings (B-2…B-9) are preserved. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
…M1/M2)
Start Option B — the conformance harness. New workspace crate
appthere-conformance (forbid(unsafe_code), no Text-specific assumptions) with
the three axis modules:
- schema (Axis 1, M2): SchemaValidator trait + libxml2-backed XmllintValidator.
Shells to `xmllint --noout {--schema|--relaxng}`, maps exit code + stderr to a
SchemaReport with first-class, line-located SchemaViolations. Distinguishes a
document being invalid (Ok(report{valid:false})) from the harness failing to
run (Err) — and a missing xmllint fails loudly (XmllintNotFound), never skips.
Tested end-to-end (XSD + RNG, valid/invalid, validate_bytes) with real xmllint.
- roundtrip (Axis 2, M3 core): NormalizedModel trait + first_divergence — a
sorted merge-walk that is order-insensitive and reports the first divergence
WITH a model path (left/right values), not a boolean. Tested incl. the
dropped-property and mangled-bookmark-id classes the spec calls out.
- golden (Axis 3): report types (RegionScore/PerceptualReport, worst-region) +
GoldenHarness trait. The vello_cpu candidate render, ΔE/worst-region diff, and
calibration are Spec 02 M5 (and promote loki-acid's SSIM) — fixed shape only.
Also: schemas/ vendoring directory with a provenance README (what ISO 29500 /
ODF schemas to pin and where); CI installs libxml2-utils so the schema tests run
rather than skip; appthere-conformance added to the dependency-direction gate's
exempt set (conformance harness, like loki-acid); inventory B-6/B-7/B-8 marked
in-progress.
Verified: 13 tests pass; clippy -D unwrap_used/expect_used clean; fmt + all
seven structural gates green; every file well under the 300-line ceiling.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
…real docs (B-7) Implement the round-trip axis end-to-end on the suite's format-neutral model. New `model` module (feature `doc-model`, optional loki-doc-model dep) provides `canonicalize_document` + `impl NormalizedModel for loki_doc_model::Document`: a recursive walk emitting an order-stable (path, value) canonical form that captures the semantically-significant content — paragraph structure + text, run-level character formatting (direct CharProps + Pandoc emphasis wrappers), style references, and bookmark ids (the named round-trip class). Block/Inline are #[non_exhaustive], so unhandled kinds are recorded by `kind` (structural changes still caught) and deepened later. Tested on real Document values via the differ: identical docs round-trip clean; a dropped run property, a changed property (bold→italic, both sides reported), a mangled bookmark id, dropped text, and a para→heading structural change are each caught with a model path — exactly the M3 acceptance bar. The crate's default build stays dependency-free (model is feature-gated); CI's --all-features run exercises it. 19 tests pass (13 default + 6 model); clippy -D unwrap_used/expect_used clean; fmt + all seven gates green. Remaining for B-7 (noted in the inventory): the three round-trip *shapes* (wire import→export→import through a format crate), deepen tables/metadata, and fold the 25 existing ad-hoc round-trip tests through the differ. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
…ce differ (B-7)
Adds the first concrete round-trip *shape* on top of the appthere-conformance
normalized-model differ: loki-ooxml now depends (dev-only, feature `doc-model`)
on the differ and exercises real DOCX write+read.
- `conformance_round_trip.rs`:
- `docx_round_trip_preserves_core_content` — green guard: a seed document
(paragraph, heading, bold run, bookmark span) is exported and re-imported
twice; both imported models must be divergence-free under
`first_divergence`/`canonicalize_document`. Import→export→import isolates
genuine export→reimport loss from hand-built model shape.
- `docx_reference_round_trip_surfaces_gaps` — `#[ignore]`'d: the full
reference fixture surfaces a real export gap (nested-inline `Str` dropped
at `blk0005/i0000/i0000`), reported with a model path. Ignored so CI stays
green; documents the gap honestly rather than masking it.
- Updates the Spec 02 conformance inventory B-7 row + status line to record the
first shape landing and the surfaced gap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
…ound-trip content-loss) The Spec 02 import-export-import conformance harness surfaced a real content-loss gap: DOCX export's `emit_char_props` wrote only ~9 of the run properties the importer (`map_rpr`) reads. A run formatted *only* by an omitted property — highlight, letter-spacing, all-caps, horizontal scale, shadow, kerning, complex/East-Asian fonts or size, run background, or language — exported with an empty `<w:rPr>`, collapsed to a plain run on re-import, and merged with adjacent plain runs, silently dropping the `StyledRun` wrapper and (via run merging) neighbouring text. First divergence: `blk0005/i0000/i0000` (a nested `Str` vanished). Root-cause fix: make the run-property emitter symmetric with the reader. - Extract the `<w:rPr>` serializers into a new `docx/write/run_props.rs` (the additions pushed `style_props.rs` over the 300-line ceiling; it now holds only the `<w:pPr>` paragraph serializers). - `emit_char_props` now also writes `w:caps`, `w:shadow`, `w:szCs`, `w:highlight`, `w:shd` (run background), `w:spacing` (letter spacing), `w:w` (scale), `w:kern`, `w:lang`, and the complex/East-Asian `w:rFonts` faces — every direct run property `map_rpr` reads is now round-tripped. - `run_props_tests.rs`: unit tests for each newly-emitted property. - `conformance_round_trip.rs`: `docx_round_trip_preserves_secondary_run_formatting` regression-locks the fix through the real import-export-import shape. The harness now advances 21 blocks to the next (independent, pre-existing) gap at `blk0026/i0001` (footnote-reference export hard-codes an explicit `vertAlign=superscript`); the reference test stays `#[ignore]`'d with an updated note. Updates `docs/fidelity-status.md` (Highlight / Letter Spacing / All Caps / Horizontal Scale rows) and the Spec 02 inventory B-7 row. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
…plicit vertAlign Second bug surfaced by the Spec 02 import-export-import conformance harness (at `blk0026/i0001`, once the `blk0005` content-loss gap was fixed): DOCX export hard-coded `<w:vertAlign w:val="superscript"/>` on every footnote / endnote reference run, *in addition to* the `w:rStyle` reference to the `FootnoteReference` style. The source model carried no such direct run property, so a re-import fabricated `vertical_align=Superscript` — an export→reimport instability. Root-cause fix, matching how Word structures the document: - Drop the redundant explicit `<w:vertAlign>` from the note-reference run; the marker references the character style by id only. - Always emit the `FootnoteReference` / `EndnoteReference` character styles (with the superscript) from the styles writer, emitting built-in defaults only when the catalog did not already define them — so an imported style with edited props still wins, and from-scratch documents still render the marker as superscript. With both harness-found export bugs fixed, the full comprehensive reference fixture now round-trips through import-export-import with **zero model divergence**, so `conformance_round_trip.rs`'s reference test is promoted from an `#[ignore]`'d gap-finder to a permanent green guard (`docx_reference_round_trip_is_stable`). Updates the Footnotes row of `docs/fidelity-status.md` and the Spec 02 inventory B-7 row. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
…rs + metadata (B-7) Extends `canonicalize_document` beyond paragraph/run depth: - Tables (`model/tables.rs`): walk column count, caption, and every row (head / per-body head+body / foot) with each cell's row/col span and its block content recursed through `walk_block`. A dropped row, de-merged cell, or lost cell text now surfaces with a `…/tbl/br0001/c0000/…` path instead of collapsing to a bare `kind=table`. - Metadata (`model/meta.rs`): walk core properties, custom properties (sorted by name), and the extended Dublin Core set under a `meta/…` prefix; dates use RFC 3339 for stable textual comparison. Both walkers are private submodules of `model/` reusing the parent's `push`/`walk_block`/`walk_inlines`. Four new `model_tests.rs` cases cover dropped cell text (with a table path), lost col-span, dropped title, and a changed creator. The DOCX reference guard (`docx_reference_round_trip_is_stable`) stays green — the fixture's 2×2 table round-trips cleanly under the deeper walk. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
…ance differ (B-7) Mirrors the DOCX round-trip shape for `loki-odf`'s ODT writer + reader (dev-dep `appthere-conformance/doc-model`): `conformance_round_trip.rs` exercises real ODT export+import through the normalized-model differ. - `odt_round_trip_preserves_core_content` — paragraphs, a heading, a bold run, and a 2×2 table survive export→re-import with no model divergence (uses the table canonicalization landed in the previous commit). - `odt_round_trip_preserves_secondary_formatting` — highlight, letter-spacing, all-caps, and bookmark anchors (ODT export documents these as lossless) also round-trip; guards the ODF path against the analogue of the DOCX `emit_char_props` regression. Both pass: ODT's lossless claim holds for this content. The dependency-direction gate is satisfied (conformance harness is exempt / dev-dep only). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
…pe (B-7) Adds the spreadsheet round-trip axis, the sheet analogue of the doc-model adapter: - New `sheet-model` feature + `sheet` module (`appthere-conformance`): `NormalizedModel for loki_sheet_model::Workbook` / `canonicalize_workbook` walks workbook metadata, each sheet's name, every populated cell's value/formula/style, and custom column widths. `HashMap`-keyed cells and widths are sorted before emission for deterministic order; a dropped cell, lost formula, stripped style, or renamed sheet surfaces with a `sheet0000/r0001c0002/…` path. Five `sheet_tests.rs` cases cover each. - New `loki-ooxml/tests/conformance_xlsx_round_trip.rs` (feature `xlsx`): `xlsx_round_trip_preserves_core_content` runs values, a formula, a bold/percent-styled cell, and a custom column width through real XLSX export+import via the differ — passes, so XLSX round-trips this content. The feature is independent of `doc-model` (the sheet model carries its own lightweight `DocumentMeta`). loki-ooxml's dev-dep now enables both features. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
…fer (B-7) Backstops the bespoke per-field assertions in the DOCX export→re-import tests with a whole-model first-divergence check, so any loss the targeted asserts don't explicitly verify still fails with a model path: - `metadata_round_trip.rs` — the shared `round_trip` helper now runs a second export→import cycle and diffs the canonical models (covers core + extended Dublin Core, which the deepened metadata canonicalizer now walks). - `math_round_trip.rs` — same backstop on its `round_trip` helper (MathML/OMML). - `comments_round_trip.rs` — body-flow differ backstop (comment anchors live in the content flow; the comment body is in `doc.comments`, outside the body canonical form, and stays covered by the existing asserts). Note on the "25 ad-hoc round-trip tests": on inspection the `round_trip*.rs` / `round_trip_conformance*.rs` files are **import-only smoke tests** (reference DOCX → import → assert shape), not export→re-import round-trips, so the differ does not apply to them. The genuine round-trips are the metadata/math/comments (and style-catalog) tests; the three body-model ones are now differ-backstopped. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
… + 3 shapes Updates the conformance inventory B-7 row and status line: table/metadata canonicalization deepened, all three round-trip shapes (DOCX/ODT/XLSX) wired with two harness-found export bugs fixed, and the genuine export→re-import tests folded through the differ. Records the finding that the "25 ad-hoc round-trip tests" are mostly import-only smoke tests, not round-trips. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
…ep (B-7) Lockfile sync for the new `loki-sheet-model` optional dependency on appthere-conformance and the `appthere-conformance` dev-dependency added to loki-odf for the ODT round-trip shape. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
Places the Spec 03 design document in docs/adr (the responsive breakpoint system, page-fit renderer switch, font-warning redesign, non-paginated typography, and cross-UI sweep). Audit-first per the spec's working method; the consolidated audit report follows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
… misbehavior inventory Audit-first deliverable for Spec 03 (no code changes; pending maintainer triage), mirroring the Spec 01/02 audit-then-triage flow. Records 15 findings (R-1…R-15): - Answers the Working-Method §4.1 question: CSS @media queries are unused in production (0 in loki-text/appthere-ui/loki-renderer), and the three Blitz constraints (position:fixed, box-shadow, custom properties) are real and already observed (0 instances in scope) — so D1 holds: a programmatic Breakpoint signal must be the single source of truth. - Spec 01 dependency status: the width source IS unified (one measured Viewport), but Viewport carries only inner_width_px (no zoom/DPI/page geometry, needed for D2 page-fit) and lives in the app layer, not shared UI infra. - Surfaces the threshold fragmentation the breakpoint system must unify: 768 (home tab) vs 900 (renderer switch, whose comment cites an unused 816px page) vs the spec's proposed 600/1024. - Per-surface misbehavior inventory, font-warning / renderer-switch / non-paginated-typography baselines, a breakpoint-placement recommendation, milestone readiness, and open questions for triage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
…iewport
Implements M1 per maintainer triage (Spec 03 tiers 600/1024; placed in
appthere-ui; zoom+DPI added now; harness-style window-free tests).
appthere-ui (shared UI infra):
- New `responsive` module. `Viewport` is **relocated here from loki-text** and
extended with `zoom` (1.0 = 100%) and `dpi` (default 96) for the M2 page-fit
switch; its `centred_origin_x` centring math (Spec 01 A-1/A-14) and tests move
with it. `Breakpoint { Compact, Medium, Expanded }` derives from the measured
width at 600/1024 (`BREAKPOINT_COMPACT_MAX_PX` / `BREAKPOINT_EXPANDED_MIN_PX`
in tokens), ordered so `>= Medium` is a plain comparison.
- `AtResponsiveContext` (a `Signal<Viewport>` + a memoised `Breakpoint`) plus
`use_provide_responsive` / `use_breakpoint` / `use_viewport`, mirroring the
theme-context pattern. The breakpoint memo only wakes readers on a class
boundary, not per pixel.
- 11 pure unit tests (classification, boundaries, ordering, predicates, zoom/DPI
independence) — no real window, per Spec 02's harness style.
loki-text (consumer, no second width source):
- `editing/viewport.rs` now re-exports `appthere_ui::Viewport` so existing
`crate::editing::viewport::Viewport` paths keep working.
- App root provides the responsive context; the editor funnels its **one**
measured `scroll_metrics.client_width` into it.
- The three viewport-driven effects (mount seed, view-mode-by-width, and the new
context publish) are extracted from the over-ceiling `editor_inner.rs` into a
new `editor_responsive.rs`, shrinking `editor_inner` 1044→1002 (baseline
tightened) rather than growing it.
Updates the Spec 03 audit report (R-2/R-3/R-4/R-5 resolved; M1 done). M2–M5
remain, audit-first per finding. fmt + workspace clippy + all gate scripts pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
Replaces the bare `width < 900` renderer guess (D2): the paginated↔reflow
switch now follows **content fit, not device class**.
appthere-ui (shared, testable):
- New `responsive::page_fit`. `resolve_page_fit(viewport, page_width_px,
currently_paginated)` returns a format-neutral `PageFit { Paginated, Reflow }`:
paginated when the page column (page width × `viewport.zoom` + a per-side
gutter) fits the measured width, reflow otherwise. All math is CSS-px, so it is
DPI-independent. The decision is **hysteretic** on the current mode
(`PAGE_FIT_HYSTERESIS_PX` dead-band), so a window dragged to the boundary can't
thrash. Tier/gutter/hysteresis constants live in `tokens::layout`.
- 6 window-free tests (Spec 02 harness style): landscape-phone-fits-paginated,
narrow-desktop-reflows, a drag sweep asserting exactly one transition in each
direction (no thrash), dead-band stickiness, zoom scaling, and unmeasured.
loki-text:
- The editor's view-mode effect (`editor_responsive.rs`) now resolves the mode
via `resolve_page_fit`, reading the document's real `page_width_px` from
`DocumentState` instead of the 900 px constant (deleted). Zoom is fixed at 100%
until zoom lands; the rule already scales by `viewport.zoom`, so it goes live
for free then. `editor_inner.rs` held at its 1002-line baseline (comment
tightened to offset the new argument).
Updates the Spec 03 audit report (R-7/R-8 resolved; M2 done). M3–M5 remain.
fmt + workspace clippy + all gate scripts pass; 17 responsive tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
Replaces the full-width wrapping band (the named offender) with a
compact-by-default, expand-on-demand, breakpoint-aware warning (D3).
loki-text — new `editor_font_warning::FontWarning` component:
- Default: a one-line compact chip ("N fonts substituted") with expand +
dismiss affordances — a small fixed footprint, not a paragraph.
- Expanded: a structured missing→substitute→action view that is **breakpoint-
aware** (uses M1's `use_breakpoint`): a compact table on Expanded width, a
vertical stack of cards on Compact — never a band that wraps.
- Severity model: metric-compatible substitutes (Carlito/Caladea/Gelasio/
Liberation*, Spec 02 §7.3) are styled calmly; material fallbacks prominently.
UI-side heuristic with a documented removal condition for when the engine
exposes severity directly.
- Dismiss is **recoverable**: a generic `AtStatusBar` notice chip
(`notice_label`/`on_notice_click`, reusable by the other apps) restores it.
- Blitz-clean: border/background elevation, no `position: fixed`, no
`box-shadow`, no custom properties. All strings via `fl!()` (new keys, incl. a
Fluent plural for the chip count).
- 3 unit tests for the severity / download-link / sort logic.
Extracted from the over-ceiling `editor_inner.rs`, shrinking it 1002→878
(baseline tightened). appthere-ui `AtStatusBar` gains the generic notice chip.
Updates the Spec 03 audit report (R-9/R-10/R-11/R-12 resolved; M3 done). M4–M5
remain. fmt + workspace clippy + all gate scripts pass; i18n + unit tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
Fixes the "cramped" non-paginated view (R-6 / D5): the GPU reflow path no longer runs the measure edge-to-edge on wide windows. loki-renderer — one shared source of reflow width (Spec 01 discipline): - `render_layout`: `MAX_REFLOW_TILE_PX = 820` (the HTML-fallback precedent) plus `reflow_tile_width_px` (viewport width capped at the cap) and `reflow_content_width_pt` (tile minus side insets, floored at MIN). Below the cap the tile tracks the viewport (narrow screens use full width); at/above it the tile stops growing so the renderer's `margin: auto` centres the column. - `document_view` (paint) caps the reflow tile via `reflow_tile_width_px`; the HTML fallback (`reflow_view`) now references `MAX_REFLOW_TILE_PX` instead of a hardcoded 820, so both reflow paths read identically. loki-text — paint, hit-test, and keyboard nav derive from the same functions so they stay aligned (the bug class Spec 01 fought): - `editor_pointer` hit-test uses `reflow_content_width_pt` and centres its origin on the capped `reflow_tile_width_px` (was the full client width). - `editor_keydown` reflow nav uses `reflow_content_width_pt`. Both drop their ad-hoc `(width·PX_TO_PT − 2·padding)` copies; `PX_TO_PT` is now exported once from `render_layout`. 3 unit tests (narrow uses full width; wide caps & centres; content = tile − insets, floored). Responsive *type scale* deferred — rescaling the document's own point sizes is a fidelity concern; the bounded+centred measure is the readability fix. Updates the Spec 03 audit (R-6 resolved; M4 done) and the fidelity-status reflow row. fmt + workspace clippy + all gate scripts pass; over-ceiling files held at baseline. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
Adds the downward view of the single-parent style tree the M4 tree view and its impact preview read (the M1 layer had only the upward ancestors view). loki-doc-model/src/style/tree.rs (new): - para_children(id): direct children, in catalog order. - para_descendants(id): transitive descendants, breadth-first, cycle/depth-guarded. - dependents_affected(base, has_local): the *exact* set of descendants whose value of a property would change if it changed on `base` — descendants that inherit it from `base` (no closer local override), covering both changing an existing override and adding a new one; subtrees shadowed by a closer override are excluded. This is M4's "preview the exact dependent set affected before Apply". - Reuses para_ancestors (M1) for the per-descendant walk; para_reparent_cycles (M1) already guards re-parenting. - 7 tests: children/descendants (incl. cycle termination) and impact preview (inherit, shadowed-subtree exclusion, new-override). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
Shows which dependent styles a staged change will also change, before the user commits — no surprise cascades (§7). - style_impact.rs (new): affected_dependents(catalog, id, changed) unions loki_doc_model's dependents_affected over the staged properties (mapping each StyleProperty to its "is set locally" shadowing test), deduplicated. 4 tests: nothing staged, alignment change excludes overriding dependents, multi-property union without duplicates, leaf has none. - editor_style_editor/mod.rs: derives the changed (staged) properties from the inspector rows, computes affected dependents on the committed catalog, and maps them to display names. - provenance.rs (StyleProvenanceList): renders an impact-preview banner naming the affected dependents when a staged change touches them. - i18n: style-impact-preview. Remaining M4: the full hierarchy tree visualization and cycle-guarded re-parenting in the form (para_reparent_cycles from M1 is the guard). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
The style editor's based-on (parent) field can re-parent a style; Apply now rejects a change that would make the inheritance tree non-acyclic (M4 acceptance: "re-parenting ... can't form a cycle"). - form.rs Apply: when the draft sets a non-empty parent, check StyleCatalog::para_reparent_cycles (M1) on the committed catalog; if it would cycle (incl. self-parent), surface a status message and abort the Apply instead of writing a cyclic parent. - StyleEditorSync gains a save_message sink (threaded from editor_inner) so the rejection is reported through the existing status banner rather than swallowed. - i18n: style-reparent-cycle. Remaining M4: the full hierarchy tree visualization (the downward para_children/ para_descendants model from the previous increment backs it). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
Completes M4: the style-editor's left column is now a hierarchy picker instead of a flat alphabetical list. - style/tree.rs: para_forest_preorder() returns the paragraph-style forest in pre-order (parents before subtrees) with per-node depth, cycle-safe (stranded cyclic styles surfaced once). 2 new tests (9 total). - editor_style_catalog.rs: catalog_style_tree() maps it to (id, display, depth), replacing the flat catalog_style_list. - editor_style_editor/mod.rs: the left column renders each style indented by depth, so the parent → child hierarchy is visible; clicking a node still opens it in the inspector. With the impact preview, cycle-guarded re-parenting, and this tree view, M4 is complete (audit updated). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
Adds the M5 keystone: deleting a style must handle its dependents predictably (§8). StyleCatalog::delete_paragraph_style(id) re-parents the deleted style's direct children to its own parent (their grandparent), so the inheritance tree stays connected and no child is orphaned; catalog order is otherwise preserved, and a deleted document-default falls back to that grandparent. Returns the re-parented children for the caller's confirmation message. 5 tests: middle-style reparents children to grandparent, root deletion makes children roots, leaf deletion, absent-style no-op, and default fall-back. The model provides the mechanism; the UI layer applies the built-in-vs-user policy (is_builtin, M2) — delete button + create-from-parent land next. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
Wires the delete-with-orphan-handling primitive into the editor. - editor_style_catalog.rs: perform_style_delete snapshots the catalog, refuses built-in/default styles (is_builtin, M2), deletes with child re-parenting (delete_paragraph_style, M5 primitive), persists to Loro, and relays out; returns the re-parented count or a DeleteError. - editor_style_editor/actions.rs (new): delete_button — a Delete control shown only for user styles; on click it deletes, reports how many children were re-parented via the status banner, and closes the panel. Extracted so form.rs stays under the 300-line ceiling. - form.rs: mounts the delete button in the Apply row (gated on draft.is_custom). - i18n: style-delete-label/-aria/-success/-builtin. Remaining M5: create-from-parent (pick a parent when creating a new style, so it opens with everything inherited ready to override). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
Completes M5. "+ New" now defaults a new style's parent to the current committed style (empty when the current style is itself uncommitted), and leaves all props unset — so the new style opens inheriting everything, ready to override selectively (the M5 "shows all inherited values ready to override" acceptance). The based-on field (with the M4 cycle guard) lets the user change the parent. - editor_style_editor/mod.rs: derive new_style_parent from the current style and pass it into the "+ New" draft; dropped the old hardcoded Left alignment so alignment inherits too. Trimmed two comments to keep the file under the ceiling. With delete-with-orphan-handling, built-in protection, rename, re-parent, and create-from-parent, M5 is complete (audit updated). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
Starts M6 (remaining families) with the character family's keystone, mirroring M2's paragraph inspector-row model. - style_char_inspector.rs (new): character_inspector_rows(&catalog, &id) resolves a standalone CharacterStyle's own inheritance chain via resolve_char_chain (M1 — the standalone-character resolver the collapsing resolve_char never provided, audit SM-2), emitting the same InspectorRow shape the paragraph family uses so the provenance view and reset/jump affordances are shared across families (the §9 uniform inspector). Character styles carry only character properties, so the rows are font family / size / bold / italic; inherited rows name the source ancestor from the character style map. - 4 tests: all-four-properties listed, local, inherited-names-ancestor, unknown. The model lands before its panel UI; mounted with a narrow, documented #[allow(dead_code)] + TODO(spec-05-m6) until the character style panel wires it (next increment). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
Wires the character inspector model into the §9 linked family: the paragraph inspector now shows the paragraph style's linked character style's rows read-only beneath the paragraph rows — both aspects on one surface — removing the character model's deferred-wiring allow. - panel_data.rs (new): inspector_data() computes everything the provenance column renders (staged rows, impact preview, new-style parent, and the linked character rows from linked_char_style via character_inspector_rows). Extracted from mod.rs, which drops to 257 lines. - provenance.rs: StyleProvenanceList gains a `linked` prop; a new LinkedCharSection component renders the linked style's name + property · value · provenance rows. - i18n: style-linked-heading. Remaining M6: a dedicated character panel (family selector + char tree/form) and the list family; table and page stay deferred (renderer support for table conditional regions; the page_styles catalog representation of ADR-0012). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
Add a browse-and-inspect surface for character styles to the style editor panel. The left column now lists every `CharacterStyle` under a "Character styles" heading; selecting one shows its own resolved rows read-only in a right-hand inspector column, reusing the shared `CharRowsSection` provenance renderer. - `char_browser::char_list_section` renders the character-style list (extracted to its own module to keep `mod.rs` under the ceiling). - `panel_data::char_data` derives the sorted `(id, display)` list and the selected style's `(name, rows)`; new `CharListEntry`/`CharSelection` type aliases satisfy clippy::type_complexity. - `provenance::LinkedCharSection` generalized to `CharRowsSection` (heading + rows) so the linked and standalone character surfaces share one renderer. - Selection threads through a single `editing_char_style` signal (avoids the Props: PartialEq blocker that keeps the panel a plain fn). - i18n: `style-char-family-heading`. Char-style editing (a char-props form) is a later increment per §9 "equal depth not required in v1". Audit M6 entry updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
Add the non-inheriting list family to the style editor panel, completing the actionable M6 families (table + page remain model-gated). List styles do not inherit (ADR-0004: a flat vector of per-level definitions), so there is no provenance chain to resolve. The new pure `style_list_inspector::list_inspector_rows` flattens a `ListStyle` into one read-only row per indent level — label kind (bullet char / numbered scheme / none) plus geometry (start indent, hanging indent, alignment). - `list_browser::list_list_section` renders the left-column list-styles list under a "List styles" heading (mirrors `char_browser`). - `panel_data::list_data` derives the sorted `(id, display)` list and the selected style's per-level rows. - Both non-paragraph read-only columns now share `family_inspector::family_inspector_columns` (character reuses `CharRowsSection`; list renders per-level), which sheds the inline char block from `mod.rs` and keeps it under the ceiling (280 lines). - Selection threads through a new `editing_list_style` signal. - i18n: `style-list-family-heading`, `style-list-level-label`, `style-list-level-detail`. 4 new pure tests (22 style tests pass). Audit M6 updated: character, linked, and list families shipped; table (needs renderable `TableProps` conditional regions) and page (needs the ADR-0012 `page_styles` catalog representation) stay deferred with rationale. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
Make the style editor panel fully usable at the Compact (< 600 px) breakpoint (§11), completing Spec 05's milestone chain. A pure, tested `StylePanelPosture::for_breakpoint` (mirrors `appthere_ui::PanelPosture`) maps the size class to layout: - Compact: the body switches from side-by-side columns to a full-width stacked sheet — taller height, `flex-direction: column`, vertical scroll (no horizontal overflow) — and every nav control meets the 44 px `TOUCH_MIN`. A segmented Edit / Inspect switcher (`body::section_switcher`) chooses which stacked group is visible so the tree, families, and inspectors are all reachable without horizontal scrolling. - Medium/Expanded: the existing side-by-side surface is unchanged. The panel is a plain fn (its `Arc<Mutex<DocumentState>>` is not `PartialEq`, so it cannot host `use_breakpoint()`); `editor_inner` — a component — reads the breakpoint and threads it in alongside a `style_panel_inspect` signal for the switcher. Each section, browser, and inspector consumes the posture for its width + touch targets. Decomposition held the 300-line ceiling: the left column + switcher moved to `body.rs`, the posture to `posture.rs` (mod.rs 243, provenance.rs 298, editor_inner 863 < 878 baseline). 2 new posture tests (24 style-panel tests pass). Audit M7 marked shipped; the M2 AtPanelHost deferral is resolved (a bespoke posture, not the narrow side-panel host). Tree breadcrumb/drill-down deferred with rationale (the indented tree stays operable at Compact). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
Add the Spec 06 design doc (benchmarking & continuous memory tracking) and, per its audit-first mandate (§1/§4.1), a ground-truth audit companion — matching the audit-then-triage flow of Specs 01/02/05. The audit inventories what is measurable today, the caches and their bounding, and Loro history behavior before any harness is built: - Measurability: Criterion is wired only in loki-layout; loki-acid has a hand-rolled open-latency bench (cold/warm) + loki_text::open spans; dhat, a root harness, a scale corpus, and a baseline are all absent. - Cache bounding: the shaping cache (ParaCache, 2-gen ~LRU, cap 2048, cleared on load) and GPU texture tiers are bounded and proven, and tile virtualization is now wired; inactive-tab layouts and per-tile font bytes remain unfixed wastes. - Loro history: oplog/tombstones never compact (a real time-driven grower); undo is bounded to 100 steps; throttled loki_text::mem counters already exist for the leak bench to consume. - Arc steady-state: shared Renderer/PaginatedLayout/FontResources confirmed shared behind Arc — the M3 guard is well-founded. - Correction (BM-3): the portable vello_cpu render-cost proxy the spec leans on does not exist yet — Spec 02's CPU path is still pending, so M2's proxy and M6's parity cadence are blocked upstream. Records finding register BM-1…BM-14 and an M1–M6 readiness triage. No code changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
Add the shared benchmarking & continuous-memory-tracking harness at the monorepo root, implementing Spec 06's two-axis split with Criterion and dhat wired. - `Axis` / `Metric` (src/axis.rs) encode §5 in code: `Metric::axis()` maps allocation bytes/counts, op counts, and vello_cpu render cost to Portable (the continuously-tracked signal, decision D1) and wall-time/RSS/frame-time to DeviceBound. Tested headless. - `measure(|| workload) -> AllocStats` (src/memory.rs) captures portable heap allocation bytes/counts via dhat; `dhat_global_allocator!()` opts a bench binary into recording. - Criterion wired via benches/: `portable_smoke` (timed) and `portable_alloc` (dhat signal) both run headless with no GPU; `device_frame_time` is feature-gated (`device`, off by default) and clearly marked hardware-only — it prints a skip notice headless and pulls no GPU deps (the GPU/RSS measurement lands in M5). Accept (verified headless, no GPU): `cargo test -p loki-bench` (5 pass); `portable_alloc` reports a real dhat signal (80000 bytes / 1 block for a 10k-u64 Vec); `portable_smoke` runs under Criterion; `device_frame_time` prints its hardware-only notice. The device axis is compiled but inert without `--features device`. Per-target portable benches (M2), the committed baseline + diff (M3), leak detection (M4), device benches + budgets (M5), and the parity cadence (M6) build on this skeleton. The vello_cpu render-cost proxy remains blocked on Spec 02's CPU path (audit finding BM-3). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
Add five harness=false allocation benches, each measuring a §6 target via `loki_bench::measure` (dhat) over a shared scale corpus, plus the corpus/report support module. All run headless and produce stable, hardware-independent allocation metrics. - portable_style_resolution (headline) — sweeps inheritance depth × chain count; the depth×count scaling is visible (depth=64 chains=1000 ~3.4 MB / 38k allocs; depth=1 resolves Local with zero allocations), surfacing the §6 super-linear watch. - portable_layout — cold-paragraph-cache `layout_document` across tiers (font scan amortised out of the measured region). - portable_model — `loro_to_document` rebuild (the typing-latency model half) across tiers. - portable_io — DOCX export (save) + import (open); the open region borrows the byte slice so only the parse allocates. - portable_export — DOCX vs ODT emission, for format comparison. `benches/support/mod.rs` generates the scale corpus (deterministic doc builder + deep-inheritance style-chain builder + Small/Medium/Large tiers) and shared metric-row formatting. The vello_cpu render-cost proxy is deferred, not built: it needs Spec 02's CPU render path, which does not exist yet (audit BM-3). PDF/EPUB export (which drive the layout pipeline) are a noted follow-up within the export target. Verified headless: all five benches run and report metrics; `cargo test -p loki-bench` still 5/5; clippy --all-targets clean. Audit M2 marked shipped. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
Record loki-doc-model / loki-layout / loki-ooxml / loki-odf as loki-bench dev-dependencies (the workloads the Spec 06 M2 portable benches drive). Lockfile-only follow-up to e3f26b3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
Add the committed-baseline + diff mechanism and the Arc steady-state guard, closing Spec 06 M3. - Pure `baseline` module (lib): `Baseline` parse/render (a stable, comment-headed, one-line-per-key text file) + `diff` classifying each key New/Removed/Regressed/Improved/Unchanged under a jitter tolerance (allocation counts tight, byte totals looser to absorb DOCX zip-ordering drift). 7 unit tests, incl. the acceptance cases: a growth regression and an Arc-share→value-clone regression (0 → nonzero = +INF) both flag Regressed. - `baselines/portable.txt`: the committed baseline, 18 curated keys — a representative metric per §6 target × Small/Medium/Large tier, plus the Arc guard metric. - `baseline` bench: collects the samples, diffs against the committed file, prints deltas for review (never a CI failure, §11); `-- --update` rewrites the baseline intentionally so git history tracks how memory moves over releases. - `arc_steady_state` bench: asserts `Arc::clone` of a real shared resource (FontResources) allocates exactly 0 — the deterministic tripwire for a share→value-clone regression toward the old per-instance duplication (audit §4). Renderer's identical guard is device-bound (M5). Verified end-to-end headless: baseline generates (18 keys), diff reports all-ok against it, a perturbed baseline surfaces `[REGRESSED] +94.7%`, the Arc guard passes (0/0); `cargo test -p loki-bench` 12/12; clippy --all-targets clean. Audit M3 marked shipped. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
Add residual live-heap measurement + a pure leak classifier, and two benches covering §7's three leak culprits. - `leak` module (lib): `residual_after` measures the heap still live after N open/edit/close cycles; pure `classify_leak` returns Bounded/Leaking by whether residual scales with reps (4 unit tests). - `leak_detection` bench — Arc-cycle + unbounded-cache culprits. The real open→edit→close cycle returns to 0 B residual (leak-free: loro fully frees on drop), while a seeded retained-document leak (1.9 MB → 124 MB over 1→64 cycles) and a seeded unbounded cache (64 KB → 4.2 MB) are both caught. Asserts both seeded leaks flag and the clean cycle does not — the M4 acceptance. - `leak_loro_history` bench — the Loro culprit. 5000 keystrokes (insert+delete, length stable) grow the oplog +10000 ops (2/keystroke, ~550 B each), measured and reported via len_ops/len_changes + retained live heap. Quantifies Finding 6 / TODO(loro-compaction) as the yardstick a future compaction fix is validated against. Production ParaCache/GPU-tier boundedness stays proven where it already is (loki-layout ParaCache unit tests; device tiers → M5); the seeded unbounded cache here proves the detector. Verified headless: leak_detection catches both seeds with the clean cycle at 0 B; leak_loro_history reports the growth; cargo test -p loki-bench 16/16; clippy --all-targets clean. Audit M4 marked shipped. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
…ec 06 M5) Ship M5's agent-runnable half (peak RSS + budgets, verified headless) and scope the device-only half (GPU frame-time) honestly. - `rss` module (lib): OS peak/current RSS via Linux /proc/self/status (VmHWM/VmRSS) + a pure, tested parser (3 tests). The number is device-local; the Linux mechanism runs and is verified in the agent. macOS/Windows readers (getrusage / GetProcessMemoryInfo) are the on-device follow-up (this forbid(unsafe_code) crate leaves the FFI to the device build). - `budget` module (lib): `check`/`headroom_frac` + `Budgets` parse/render (4 tests). Budgets are review targets, never gates (§11). - `device_rss` bench: measures peak RSS per corpus tier (held live) and reviews it against committed budgets; `-- --update` calibrates budgets at 1.5× measured peak. Agent run: small 9.3 / medium 12.1 / large 22.9 MiB → budgets 13.9 / 18.2 / 34.4 MiB. - `baselines/rss_budgets.txt`: the committed per-tier budgets. - `docs/adr/spec-06-calibration.md`: the committed calibration record (measured distributions, device/date/tool versions, budgets + formula) with the explicit BM-14 caveat that agent numbers UNDER-count real devices (no GPU textures) and must be re-measured on the Windows+RTX 3050 / MacBook A16 before they are authoritative. - `device_frame_time`: refined to state the wgpu timestamp-query approach; the one M5 metric that cannot execute or be verified headless — it stays device-gated (needs a GPU), documented for the on-device pass. Verified headless: device_rss measures real per-tier RSS and reviews against budgets; cargo test -p loki-bench 23/23; clippy --all-targets clean (default + device). Audit M5 marked shipped (agent half). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
Close Spec 06's final milestone: write down the tracked-not-gated discipline (§11) and establish the CPU/GPU parity cadence (§12), with the version-bump trigger made mechanical. - docs/adr/spec-06-discipline.md: the standing process — the before/after-change run → review → intentional-update workflow over the committed artifacts (portable baseline, RSS budgets, parity marker), and the parity triggers (mechanical Vello bump + calendar cadence), where it runs (GPU hardware), and the actionable divergence response. - `parity` module (lib): pure `vello_version_from_lock` / `parity_status` / marker parse+render (6 tests). Makes "on every Vello bump" detectable. - `parity_status` bench: reads the pinned vello (0.6.0) from Cargo.lock, compares to the committed marker, flags a bump as [DUE]. Headless, so the trigger is verifiable in the agent; `-- --update` records a confirmed run. - baselines/parity_marker.txt: committed in a deliberately UNCONFIRMED state — the parity check needs a GPU + Spec 02's vello_cpu path (BM-3), so no on-device run exists yet and the tool correctly reports [DUE]. Recording a confirmed version now would silently bless unchecked goldens. Spec 06 M1–M6 are now shipped for the agent-runnable scope; the remainders are genuinely device-/upstream-gated (GPU frame-time execution, on-device RSS recalibration BM-14, vello_cpu proxy + parity execution BM-3). Verified headless: parity_status reports [DUE] honestly; cargo test -p loki-bench 29/29; clippy --all-targets clean. Audit M6 marked shipped. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
Catalogue every documented deferral across code and docs, and — the point of the pass — verify each against the current code to catch cases where a doc still says "deferred" but the code already does it. Produced by five parallel verification passes over: Rust TODO(topic) annotations (~70 / 34 distinct), COMPAT workarounds + dependency patches (~70 / 6), Spec 01-06 docs, prior audit docs (memory/perf/security/ fidelity), and fidelity-status.md / CLAUDE.md tech-debt / MVP scope / format ADRs. Headline (§1): 9 stale-documentation findings where the code has moved on — e.g. memory-audit "Finding 2 virtualize" is now implemented (virtualize.rs), TODO(editing) next_style_id is wired, the COMPAT(dioxus-native) "position:absolute unsupported" notes are contradicted by CLAUDE.md's confirmed-CSS list, audit-2026-06-10 C1 + S1-S4 security findings are mitigated, fidelity gap #14 is done, and two CLAUDE.md rows (DublinCore export, worst-offenders ceiling sizes) are stale. Plus a Spec 02 "resolved-as-decision but not built" caveat (Gelasio, schemas, goldens, SSIM, vello_cpu). The rest (§2-§7) are genuine, correctly-documented deferrals: in-code TODOs (upstream-gated Parley/Blitz/Vello + UX polish), COMPAT clusters, 6 upstream-gated dep patches (loki-file-access the one locally actionable), per-spec deferrals, prior-audit open findings, tech debt (35-file ceiling backlog, tab_stops/background_color Loro round-trip gaps), and out-of-MVP Calc/Slides scope. §8 lists the doc-hygiene fixes. No code changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
Apply the doc-hygiene fixes from the deferred-features audit §8 — the places where a doc still said "deferred/open/unsupported" but the code already does it. No behaviour changes; comments + status markers only. - S-1 `COMPAT(dioxus-native): position:absolute unsupported` (5 sites: ribbon/group, page_tile, editor_inner ×3, form_font) — reworded to note block-level absolute is now confirmed (CLAUDE.md), so the in-flow layouts are deliberate choices, not Blitz limits; form_font keeps the inline-formatting-context caveat (still unverified). editor_inner edits are line-neutral (at its baseline). - S-2 `TODO(editing)` on `next_style_id` → plain doc note (the Enter/split behaviour is wired in editor_keydown_ctrl). - S-3 `TODO(super-sub)` (para.rs ×2) — reworded: the vertical shift is applied in para_emit; only the native Parley API is missing. Kept line-neutral (para.rs is a baselined over-ceiling file). - S-4 memory-audit Finding 2 (virtualize tiles) → Fixed (virtualize.rs). - S-5/S-6 audit-2026-06-10: dated banner marking C1 + S1–S4 resolved. - S-7 fidelity-audit gap #14 (horizontal text scale) → RESOLVED (resolve.rs:387-390 forwards .scale). - S-8 CLAUDE.md DublinCore row — corrected: core + extended Dublin Core now write back to DOCX (core.xml + custom.xml) and ODT (meta.xml); only custom user props / editing-duration / app.xml remain. - S-9 CLAUDE.md worst-offenders ceiling table refreshed from the current baseline (para.rs 1979 / flow.rs 1953 / …; the old sizes were stale). - Spec 02: status + Gelasio line clarified — the "✅ Resolved" B-items (vello_cpu path, schemas, goldens, SSIM, Gelasio) are decisions, not built. Gates: fmt, file-ceiling (para.rs held at 1979), license all pass; affected crates compile clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
The merge of main into this branch (cce9772) left the workspace unbuildable, failing both CI checks. Repair, no functional changes: - Cargo.toml: the [workspace] members list had a DUPLICATE `members` key (invalid TOML) — main's list and this branch's list were both kept instead of unioned. Merged into one list (loki-bench + appthere-conformance from here, plus main's server crates). This un-breaks `cargo metadata`/build (the build-and-test blocker). - Cargo.lock: the merge corrupted it in several places — two unclosed `dependencies` arrays (backtrace→axum, loki-bench→loki-convert, the latter also dropping loki-bench's `thiserror` dep), a lost oorandom `source`/`checksum`, and a duplicate `once_cell` entry. Hand-repair surfaced cascading breakage, so the lock was regenerated from the now-valid manifest (`cargo generate-lockfile`). Pins held: vello 0.6.0, wgpu 26, sqlx 0.8.6, zip 2.4.2 — no major bumps. - loki-server + loki-headless: the two new binary crates from main lacked the required `#![forbid(unsafe_code)]` (Spec 01 A-7 unsafe policy) — the lint blocker. Added it; both crates contain no `unsafe`, and every sibling lib crate already forbids, so this matches the convention. Verified locally: manifest + lock resolve cleanly; loki-bench builds; unsafe-policy / license / no-panics / file-ceiling gates all pass. The full server-stack build (sqlx/axum) is left to CI, which has the offline build env; those crates already built on main. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
…ection gate The main-merge brought 11 server crates into this branch, where the Spec 01 dependency-direction gate (check-dependency-direction.py / ADR-0009) enforces layered dep flow — but they (and loki-bench) weren't in the layer model, failing `lint`. Registered them: - loki-bench → EXEMPT (benchmark harness, like loki-acid / appthere-conformance). - Server subsystem → a new L7–L11 tier above the app binaries, ordered by its own dependency graph so every edge is downhill (verified): L7 loki-model/ loki-crypto/loki-server-audit/loki-print, L8 loki-server-store/loki-server-auth/ loki-convert, L9 loki-server-collab, L10 loki-server-api, L11 loki-server/ loki-headless. ADR-0009's layer table updated to match. Gate now passes locally (97 edges, all downhill, 35 mapped crates). Framing note for the maintainer: this folds main's backend crates into ADR-0009's (originally client-side) layer model; if the server subsystem should have its own layering doc, the tier here can move. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF
… gap) The main-merge added a `path: Vec<PathStep>` field to `loki_layout::PageParagraphData` (nested-nav) and updated the loki-text initializers, but missed the cross-crate fixture in loki-renderer's render_layout_tests — E0063 broke the loki-renderer lib-test build (build-and-test). Set `path: vec![]` (top-level, matching the fixture). Verified: `cargo test -p loki-renderer --no-run` compiles clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This is a large audit-first delivery spanning six specification documents (Spec 01–06), a new conformance testing harness (
appthere-conformance), responsive UI foundation (breakpoint+viewport+page-fit), benchmarking infrastructure (loki-bench), and targeted production code changes to support nested-block editing, style resolution, and table/note round-tripping.No breaking changes to public APIs. All changes are additive or internal refactoring (test extraction, license-header standardization).
Key Changes
Specification & Architecture Documents
.rsfiles, 25 workspace crates, Rust 2024 edition. Companion audit report documents findings and proposes ADR-0009 (target layering).*Propsnaming convention, style resolution provenance, conditional panels as components.Conformance Testing Harness (
appthere-conformance)model/mod.rs: Format-neutral document model + canonicalization for DOCX/ODT round-trip stability.roundtrip/mod.rs: Import-export-import axis with differ + deepened canonicalizer for tables/notes.schema/mod.rs: Schema validation via libxml2 (xmllint).sheet/mod.rs: Conformance test sheet (CSV-like registry of test cases + results).conformance_round_trip.rs(DOCX/ODT),model_tests.rs,roundtrip_tests.rs,sheet_tests.rs.Responsive UI Foundation
appthere-ui/src/responsive/: New module with breakpoint system, viewport tracking, and page-fit logic.breakpoint.rs: Breakpoint enum +use_breakpoint()hook (Spec 03 M1).viewport.rs: Viewport size tracking via Dioxus context.page_fit.rs: Page-fit switch (fit-to-width / fit-to-page) with reflow integration.breakpoint_tests.rs,viewport_tests.rs,page_fit_tests.rs.appthere-ui/src/components/panel_host.rs: Sanctioned host for conditionally-mounted panels (Spec 05 §10 / ADR-0013).loki-text/src/routes/editor/editor_font_warning.rs: Font-substitution warning UI (Spec 03 M3 / D3), compact-by-default, expand-on-demand, dismissible, breakpoint-aware.https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF