Skip to content

Spec audit reports + conformance harness + responsive UI foundation#29

Merged
kevincarlson merged 87 commits into
mainfrom
claude/adr-docs-setup-ogwz5a
Jul 4, 2026
Merged

Spec audit reports + conformance harness + responsive UI foundation#29
kevincarlson merged 87 commits into
mainfrom
claude/adr-docs-setup-ogwz5a

Conversation

@kevincarlson

Copy link
Copy Markdown
Member

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

  • Spec 01 (Codebase Audit & Target Architecture): Inventory of 664 tracked .rs files, 25 workspace crates, Rust 2024 edition. Companion audit report documents findings and proposes ADR-0009 (target layering).
  • Spec 02 (Conformance Testing Harness): Defines three audit axes (schema validation, round-trip stability, canonicalization). B-1 + B-10 resolved; M1–M3 skeleton + schema axis landed.
  • Spec 03 (Responsive UI Foundation): Audit complete; M1–M5 implemented (breakpoint foundation, page-fit switch, font-warning redesign, bounded reflow typography, cross-UI sweep).
  • Spec 04 (Ribbon UI Refinement): Audit complete; M1 (framework + rename) + M2 (labeled-group standardization) implemented.
  • Spec 05 (Style Management Panel): Audit complete; M1 (provenance resolution) is the keystone. Triaged build order documented.
  • Spec 06 (Benchmarking & Continuous Memory Tracking): Audit complete; ground-truth inventory + finding register (BM-1 … BM-14) + triaged M1–M6 roadmap.
  • ADR-0009 (Target layering & crate-dependency invariants): Proposed architecture for dependency direction enforcement.
  • ADR-0010–0013: Per-crate licensing, *Props naming convention, style resolution provenance, conditional panels as components.

Conformance Testing Harness (appthere-conformance)

  • New crate implementing Spec 02 axes: schema validation (xmllint backend), round-trip canonicalization, and golden-file comparison.
  • 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).
  • Tests: 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.
    • Tests: 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.
  • **`loki-text/src/routes

https://claude.ai/code/session_0161wmWUYyApCrJ9tFDBsETF

claude added 30 commits June 28, 2026 15:44
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
claude added 22 commits July 2, 2026 05:10
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
@kevincarlson kevincarlson self-assigned this Jul 4, 2026
kevincarlson and others added 4 commits July 4, 2026 10:01
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.
@kevincarlson kevincarlson merged commit 7c83d6d into main Jul 4, 2026
2 checks passed
@AppThere AppThere locked as resolved and limited conversation to collaborators Jul 4, 2026
@kevincarlson kevincarlson deleted the claude/adr-docs-setup-ogwz5a branch July 4, 2026 23:20
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