# Technical Documentation: logseq/mobile

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/logseq/mobile)

### Overview (`README.md`)

# Logseq

Logseq is a native SwiftUI app for iOS and a Kotlin/Jetpack Compose
app for Android. Both clients share an OCaml application model and core, backed by
the OCaml DataScript library and the lui reactive UI layer.

iOS and Android share the app version in `version.xcconfig`. Android builds,
the iOS build scripts, and the TestFlight lane embed the mobile repository's
current Git commit as the settings revision.

Shared core sources live in `shared/src/logseq/core`, with executable entry points
in `shared/src/logseq/entry`. The `shared/native` directory contains Dune configuration and
native platform bridges; generated OCaml stays under `_build`. Core tests live
in `shared/test/logseq`, one alcotest suite per core module. The core test gate also builds the seed and live-sync executables.

## Repository layout

- `apple/`: Swift package, app, extensions, Xcode workspace, and Apple tests.
- `android/`: Kotlin/Jetpack Compose Android app and its platform integration.
- `shared/src/`: OCaml application and core sources.
- `shared/test/`: OCaml test suites.
- `shared/native/`: Dune configuration and native bridges.
- `tests/e2e/`: Maestro flows and fixtures.
- `scripts/`: build and validation commands, run from the repository root.
- `docs/`: design and development documentation.
- `duniverse/`: fetched OCaml dependencies; ignored by Git.

## iOS

Open `apple/Project.xcworkspace` and run the `Logseq App` scheme in Xcode.

The Apple backend comes from the `LUIAppleBackendStatic` product of the LUI
Swift package, pinned by Git revision in `apple/Package.swift` and `apple/Package.resolved`.
Use Swift 6.2 or later. Backend changes belong in the LUI repository.

The first iOS build creates a deployment-targeted OCaml 5.5 toolchain under
`_build/apple-toolchains`. Native dependencies and core objects are keyed by
compiler, target, and source fingerprints, so later builds reuse them. Set
`LOGSEQ_APPLE_TOOLCHAIN_ROOT` to share the toolchain cache, or provide
`LOGSEQ_IOS_TOOLCHAIN_PREFIX` to use an existing compatible compiler
directly.

`scripts/install-mobile-ios-device.sh` uses signing and device settings from
`.logseq-ios-device.env`. To replace an existing Logseq installation, use
`LOGSEQ_IOS_BUNDLE_ID=com.logseq.logseq` and explicitly set
`LOGSEQ_IOS_ALLOW_PRODUCTION_REPLACE=1`, with a profile and signing identity
from the same Apple team as the installed app.

Device builds use the optimized Release configuration by default. Set
`LOGSEQ_IOS_BUILD_CONFIGURATION=debug` when you need an unoptimized build for
debugging; use Release builds when comparing startup and scrolling performance.

Device builds embed both the widget and share extensions. Matching development
profiles are selected automatically; set `LOGSEQ_IOS_WIDGET_PROFILE` or
`LOGSEQ_IOS_SHARE_PROFILE` to select a profile explicitly.

## Android

Android is a standard Gradle project:

```sh
cd android
./gradlew :app:assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk
```

Release builds use `:app:assembleRelease` / `:app:bundleRelease`. The e2e
profile APK used by `scripts/test-android-e2e.sh` is `:app:assembleProfile`
(`app/build/outputs/apk/profile/app-profile.apk`). JVM unit tests run with
`./gradlew test`.

Instrumented tests default to the Debug build. To test an optimized Release,
build its matching test APK in the same invocation:

```sh
./gradlew :app:assembleRelease :app:assembleReleaseAndroidTest -PlogseqTestBuildType=release
```

Sign the application and test APK with the same certificate before installing
them. The Release test build applies the application's R8 mapping to test code.

The OCaml core shared library is built separately by
`scripts/build-android-native.sh` (per ABI), which installs
`liblogseq_core.so` into `android/app/src/main/jniLibs/<abi>`; the Gradle
`buildAndroidNativeCore` task invokes it automatically (set
`-PlogseqRequireNativeCore=false` to compile Kotlin without it).

The Android host includes Capture and Journal app shortcuts, Capture and
Today’s Journal home-screen widgets, inbound sharing, deep links, native
authentication, media services, and the OCaml core JNI library.

## iPhone shortcuts and widgets

Long-press the app icon for Voice and Quick Add.
The Shortcuts app exposes Quick Add, Record Audio, Today's Journal, and Capture
to Journal (text input). The Quick Capture home-screen widget includes recording
and capture buttons; Today's Journal opens the journal. On iOS 18 and later,
Quick Add and Record Audio are also available in Control Center and on the Lock
Screen. These entry points open the existing composer and recorder, including
when the app needs to launch first.

Both iOS build scripts include App Intents metadata and the widget extension.
Device builds select a compatible development profile for `com.logseq.logseq.shortcuts`;
set `LOGSEQ_IOS_WIDGET_PROFILE` in `.logseq-ios-device.env` to specify one.
The extension and app must be signed by the same team, and their profiles must
include the target device.

## Testing

Do not run `swift test` in this repository because the XCTest bridge hangs in
the current environment. Use the supported gates instead:
Run the model suite without that bridge using `python3 scripts/test-swift-model-suite.py`.
It builds `LogseqModelTests` and launches Swift Testing directly.
Run the native UI suite in a simulator app host with `python3 scripts/test-swift-ui-suite.py`
after building the simulator core. This also invokes Swift Testing directly.
Run `LOGSEQ_IOS_CONFIGURATION=release python3 scripts/test-ios-native-menus.py`
after a Release simulator build to exercise native composer menus with Maestro.
It checks every attachment presenter, recording import, repeated cancellation,
all task statuses, and the search close button without a server or credentials.
Add `--hit-testing` to check independent composer focus targets and the first
single-tap File selection above the active keyboard.
Check WebSocket peer closures, transport cancellation, and owner-task cancellation
with `python3 scripts/test-ios-sync-recovery.py`; add `--simulator` to run the
same real-socket checks with the iOS Foundation runtime. The simulator run also
checks that automatic pending uploads preserve the foreground subscription while
an explicit Sync now can restart it.

```sh
swift build --package-path apple --disable-sandbox --triple arm64-apple-ios17.0-simulator \
  --sdk "$(xcrun --sdk iphonesimulator --show-sdk-path)"
(cd android && ./gradlew test)
opam exec --switch=5.5.0 -- dune build @shared/test/runtest
opam exec --switch=5.5.0 -- dune build @shared/native/runtest
./scripts/test-android-e2e-runner.sh
```

For randomized multi-client sync checks, use Node.js 24 or newer. Build the current CLI bundle and
`db-worker-node` in a separate Logseq master checkout, and build `db-sync-node`
from its `deps/db-sync` directory. The CLI and worker must have matching revisions.
Then build this repository's `shared/native/logseq_sync_client.exe` target and run:

```sh
node scripts/local-sync-fixture.cjs /absolute/path/to/logseq-master
# In another terminal, use the fixture path printed above:
node scripts/run-local-sync-stress.cjs /absolute/path/to/fixture.json 20261027 320 /absolute/path/to/logseq-master
```

The fixture runs the real local sync server with a disposable database, JWT issuer,
and isolated CLI configuration. The driver uses the real mobile session and CLI,
generates all mobile semantic operation types plus capture, transcription, and
flashcard reviews, and checks actual attachment uploads. Runs of at least 177
iterations also exercise offline edits, process crashes, server restart, and lost
HTTP acknowledgements. Successful runs require empty pending queues and identical
UUID-normalized synced datoms in the mobile database, CLI database, and a fresh
server snapshot. The CLI comparison excludes its derived `block/refs` and local
`block/tx-id`, matching the server checksum exclusions; the mobile/server comparison
includes every datom. Each run retains its seed, operation trace, summary, and confirmed UI
snapshot under the temporary fixture directory. Stop the fixture with Ctrl-C.

Authentication uses Cognito Hosted UI authorization-code flow with PKCE. Apple
uses `ASWebAuthenticationSession`; Android uses Custom Tabs. Tokens are stored
in the platform secure store, so neither client depends on Amplify or the AWS
SDK.

Sync uses dev's `pull` / `pull/ok` journal and `tx/batch` protocol. Only canonical
server journal rows enter the persisted graph; pending edits store normalized
datoms and replay on a display DB without durable storage. Snapshot bootstrap
reads its cursor through the existing pull endpoint and its schema from the
snapshot database, without requiring extra snapshot metadata or response headers.

The normalization and overlay replay algorithms in `Dev_sync` are extracted
verbatim from Logseq dev commit `53952776ef10805cba03009d444cf021f0d2c3ce`.
`python3 scripts/vendor-dev-sync.py ../logseq-1 --check` verifies the pinned source;
the same command without `--check` regenerates it. Mobile platform adapters are
kept in `scripts/vendor-dev-sync-platform.ml`.

The Cognito callback is `logseq://auth/callback`; sign-out uses
`logseq://auth/logout`. Both URLs are registered in the Cognito app client's
allowed callback and sign-out URLs.

---

### Overview (`apple/Tests/ImageCompression/README.md`)

# Image import compression checks

The app's photo picker, embedded picker, camera and file picker converge on
`LGAssetImporter`. The share extension and App Group stager use the same
`LGImageCompression` policy before recording size and SHA-256.

- JPEG larger than 512 KiB: longest edge at most 2560 pixels, JPEG quality 0.90.
  Replace the bytes only when the result is at least 10% smaller. Never upscale.
- Apply all eight EXIF orientation transforms before encoding an upright image.
- Preserve PNG screenshots, alpha, GIF/APNG animation, HEIC, RAW, HDR auxiliary
  images and other formats byte for byte. Avoid downscaling long scans/panoramas
  with an aspect ratio above 3:1. Reject oversized dimensions before decoding.
- PDF, Markdown, audio and other documents bypass image processing.
- Correct a picker's filename/MIME mismatch using the actual image format.
  Every import gets a unique path, including imports with identical filenames.
- Compression runs on the import worker rather than the app's main actor.
  Image I/O decodes JPEG directly to the target resolution.

The existing Logseq master camera path (`frontend/mobile/camera.cljs`) defaults
to quality 80 and returns JPEG. Its journal insertion uses the same asset upload
path. This native policy uses quality 90 and keeps screenshots and formats with
additional semantics intact; it does not change display thumbnails or sync.

Run on macOS without XCTest or SwiftPM:

```sh
bash scripts/test-image-compression.sh _build/image-compression-evidence
python3 scripts/test-ios-share-providers.py
```

The first command tests the actual importer, App Group stager/importer and codec.
It generates repeatable textured images, text screenshots, alpha and animations.
It optionally uses the macOS Sonoma Horizon landscape when installed locally.
No personal photo or system wallpaper is checked into this repository.
The second command exercises real iOS item providers, including file and data
representations, UIImage orientation, large JPEG compression, queueing and
checksum-validated import, in an isolated simulator test app.

Validation on 2026-10-21 (decimal bytes):

| Fixture | Before | After | Reduction | JPEG PSNR |
| --- | ---: | ---: | ---: | ---: |
| Textured 4032 × 3024 JPEG | 8,790,779 | 1,220,434 | 86.1% | 44.18 dB |
| Sonoma 3840 × 2160 landscape JPEG | 5,905,442 | 1,184,826 | 79.9% | 40.08 dB |
| Text screenshot / transparent PNG / animation / HEIC | Original | Identical bytes | 0% | Lossless |

PSNR compares decoded output against Image I/O's upright 2560-pixel reference,
so it measures encoding error separately from the intentional photo downscale.
The regression floor is 35 dB. Output sizes can vary between system encoders.
The generated landscape and screenshot were also inspected visually.

---

### Overview (`apple/App/Assets.xcassets/LaunchLogo.imageset/README.md`)

Vector geometry from `logseq/logseq`, `src/main/frontend/components/svg.cljs`, `logo`.
128 pt canvas, with padding around the original 21 × 21 view box. Light: theme gray (`#6F6F6F`); dark: Logseq cyan (`#85c8c8`).
Both appearances use 20% opacity, shared by the system launch screen and the in-app transition.

---

### Overview (`docs/agent-guide/android-ios-release-qa/2026-10-21/README.md`)

# Evidence for Android / iOS release QA — 2026-10-21

Revision: `5db32d18`; app version: `2.0.2`.

- `android-demo.mp4`: unedited Android `screenrecord` output, H.264, 1080×2400, 101.07 seconds, no audio. Profile variant.
- `android-rich-content.png`: first frame extracted from the native recording.
- `ios-rich-content.png`: production iOS renderer and core in the repository's offline UI test host, same 8-journal fixture. Authentication and platform presenters are not exercised by this host.
- `android-performance.json`, `launch-*.log`, `scroll-*-gfxinfo.txt`: profile variant, initial rich-content workload. Host compilation activity overlapped parts of this exploratory sample; these are diagnostic samples, not release acceptance measurements.
- `scroll-rich.perfetto-trace`, `scroll-rich-trace-summary.csv`, `scroll-rich-cpu.csv`: 15-second Android system trace of rich-content scrolling; nested trace durations overlap and must not be added.
- `release-system-anr-invalid/`: excluded scrolling samples. A system ANR dialog intercepted gestures; zero rendered frames does not mean smooth scrolling. Launch callbacks were recorded while the dialog was present and are not end-to-end readiness results.
- `ios-navigation-*.log`: attempted UI automation. A screenshot path was rejected by Maestro; a later Settings assertion failed in the isolated host. Neither establishes an iOS shipping-app regression.
- `measure-android.py`: reproducible exploratory measurement driver. Confirm the foreground screen and absence of system dialogs before running. It does not automatically validate screen state.

Fixture creation uses the existing `logseq_e2e_seed.exe --fixture` / `--performance=100x8`. Android migrates the graph to `files/graphs/UI_Regression-uiregression`; replacing the legacy `files/graphs/uiregression` after migration does not change the active graph. An exploratory attempt against the legacy path was excluded.

Trace analysis follows [Perfetto trace analysis](https://perfetto.dev/docs/quickstart/trace-analysis) and uses `trace_processor query`. Emulator frame timing does not establish Redmi device timing.

- `release-100x8/`: post-reboot Release samples using the canonical active graph directory; 100 journals × 8 blocks, without WebViews. Includes frame statistics, memory, screenshots, and a separate 12-second system trace. Startup samples remain affected by OS/host load and are not treated as normal launch acceptance measurements.

Rerun after installing the desired variant and preparing the active graph:

```sh
rtk proxy python3 docs/agent-guide/android-ios-release-qa/2026-10-21/measure-android.py --device emulator-5554 --variant release --runs 3 --output /tmp/logseq-qa-rerun
```

The script now rejects scrolling samples with zero App frames; this is a basic validity check, not proof that every gesture reached the intended list. Inspect the foreground screen as well.

---

### Overview (`docs/agent-guide/android-release-quality/2026-10-21/README.md`)

# Android release quality evidence — initial 2026-10-21, continued 2026-10-22

Implementation, emulator verification and the final native recording are complete. This does not establish Redmi performance or production Release readiness. See [the result report](../../005-android-release-quality_report.md) for conclusions and remaining gaps.

## Accepted source and artifacts

- Root build revision: `2ad7d02e`, plus this task's Android workspace changes. Other concurrent Apple/shared-core/editor work is preserved; later workspace HEAD is not the identity of these frozen APKs.
- App LUI pin: `1edf12691bcd78ee65b52355d5c2a35f4194524c`. Equivalent source tree pushed to LUI main as `e33a152120dff61a65217130d9479f0e974fde47`, after `03cc0806`. The diff is one Android backend file, four insertions/one deletion. It adds a retained-kind query and exposes an existing decoded-batch method. No Swift/OCaml/layout/style changes.
- `patch-pipeline/final-frozen-apks.json`: frozen Debug, AndroidTest and unsigned Release checksums. `final-integration-performance/apk.json`: the locally signed final Release and native library checksums. `apks.json` retains historical packages and a clearly named `final-frozen-release` entry.
- Final native build gate: `patch-pipeline/final-build-gate.log`. The earlier incremental packaging failure and successful retry are retained as `native-build-package-failure.log` and `integration-release-success.log`; they are not hidden or counted as a passing first attempt.
- JVM tests: 54 passed, XML under `unit-tests/`. Nine backend contracts also passed on the original pipeline (`patch-pipeline/contracts-nine-original.log`). Functional renderer/accessibility/contrast and measurement RED/GREEN logs remain in this directory.
- Final frozen APK instrumentation: `patch-pipeline/final-frozen-ui.log`, **17 tests passed**. Earlier 23-test runs included the withdrawn layout experiment and do not represent the retained suite. Premature/intermediate logs are not the final integration gate.
- Measurement validity tests: eight; graph seed tests: two; existing shell E2E runner suite passed. LUI's own host-disabled Paparazzi test task is not counted as a pass.

## Accepted measurements

AVD `logseq_release_qa_33`, `emulator-5556`: API33 Google APIs arm64, 1080×2400, density420, 60Hz, four cores, 2048MiB, host GPU, headless, radios disabled. Dataset: 100 journals × eight blocks, no WebViews. The explicitly gated androidTest fixture uses an invented token and a closed localhost endpoint; it does not validate real authentication or sync.

No timing run overlaps this task's compilation, tracing, screenshots or recording. The Mac is shared, so unrelated host work remains a source of variance. Each scroll round uses four downward/four upward 400ms gestures and retains foreground/window state, middle/before/after hierarchy, frame statistics and memory. The driver rejects obstructed, nonmoving or zero-frame scroll samples. The idle 0-frame `4950ms` histogram sentinel is not idle jank.

- `simplified-cpu/`: actual Simpleperf startup/scroll samples. Patch handling/application accounts for 3.53% of sampled process CPU, not all stalls.
- `patch-pipeline/native-patches.json`: actual 50-batch native viewport stream, 8,774,118 bytes. SHA256 `849c3a1969d03a46b9697c2ce9f87a94e698273af5720bdfef1c7584b47b4530`. The original metadata table retained detached history and copied it every batch; the fix removes that duplicate state and the second JSON parse.
- `patch-pipeline/release-before.json`, `release-after.json`, `release-before-2.json`: non-debuggable Release CPU replay of the identical stream/core, six warmups plus 12 samples each. Correct medians from raw samples: 492.38 / 348.09 / 1007.37ms. `summary.json` is authoritative; provisional/debug summaries are historical. About 29% lower in the first pair is pipeline CPU only, not whole-app speed or a cross-device guarantee.
- `patch-ui-before/` and `patch-ui-after/`: controlled same-content Release scrolling, native libraries byte-identical (core SHA `51332dec49f00dcf822f44cbc2536ad25723c45f03c9f8f1f8819c86ff539932`). Original 93/743 janky frames (12.52%); new 59/949 (6.22%). Both renderers already include all rich-content fixes. Only two product Kotlin files differ (`patch-pipeline/controlled-source-differences.json`). The isolated baseline build produced path/build-ID differences in native libraries, so they were replaced with the after APK's libraries, zipaligned and locally signed; checksums are in `patch-ui-after/controlled-apks.json`. Both use reset ART state and normal packaged library profiles with speed-profile compilation.
- `final-integration-performance/`: the final frozen package with rebuilt current native core, SHA `d296f89ec569e49b2c87454cbbb21c766a3d4751efba06f7cb923b5920b021b9`. Three rounds: 11/324, 7/336, 9/333 janky frames; P95 26/23/21ms; P99 73/30/27ms. Total 27/993 (2.72%). All three meet the suggested emulator budget, but this newer core is a separate integration validation and cannot be used to inflate the controlled optimization percentage.

## Final visual evidence

`android-final.png`, `android-final-hierarchy.xml`, `final-demo-after-hierarchy.xml` and `android-final-demo.mp4` come from the final frozen Release. The video is unedited Android `adb shell screenrecord`, H.264 1080×2400, 24.829056 seconds, no audio (`final-demo-metadata.json`). It shows restored rich content, formula/code rendering and bidirectional scrolling. A still and a scrolling frame were visually reviewed. Recording was separate from timing.

## Rejected and historical evidence

The root-menu overlay experiment (`d01cb95c`) is withdrawn, absent from the pinned dependency and not pushed to main. Its extra layout/menu machinery did not justify its complexity. The candidate 2,740-rule app Baseline Profile is archived as `evaluated-baseline-prof.txt`, absent from app build inputs. Six experiment-only tests are archived as text. Normal dependency profiles remain.

API36 system/ART startup ANRs, wrong-foreground/timeout samples, failed demo navigation, provisional microbenchmarks and intermediate integration builds are retained for audit, not combined with accepted API33 measurements. [Historical experiment notes](historical-experiments.md) preserve their original context and superseded status statements.

---

### Overview (`android/app/src/androidTest/assets/image-compression/README.md`)

# Android image import compression

Photos, documents and shared URIs pass through `AndroidAssetImporter.importDocument`.
Camera capture and these URI imports finalize through `importedFile`, which calls
`AndroidImageCompression` once before reading final size, MIME and SHA-256.
Only staged application-owned files are replaced; provider originals remain intact.

## Policy

- JPEG larger than 512 KiB: longest edge at most 2560 pixels, quality 90. Keep the
  result only when it saves at least 10%; never upscale.
- Normalize all eight EXIF orientations, including mirrors, while scaling.
- Keep PNG text and transparency, GIF/APNG, HEIC, RAW, HDR/MPF/XMP auxiliary images,
  wide-gamut JPEG and scans/panoramas above 3:1 in their original encoded format.
- Correct JPEG/PNG/GIF/WebP filename and MIME mismatches without changing the
  pixels of preserved formats. Documents and audio bypass image processing.
- Inspect dimensions before decoding. Reject pixel decoding above 100 megapixels.
  Power-of-two decoder sampling bounds the intermediate bitmap allocation.
  Estimated working memory includes decoded/output bitmaps plus 8 MiB overhead;
  the budget is capped at 96 MiB and half the available application heap.
- Serialize compression working sets when multi-item shares import concurrently;
  compute the available budget after acquiring the compression lock.
- Allocation, decoding and output failures retain the original. Discard rejected
  candidates and recycle intermediate bitmaps. A low memory budget skips compression
  instead of reducing the chosen photo quality further.

The dimensions-first sampling follows Android's [large bitmap guidance](https://developer.android.com/topic/performance/graphics/load-bitmap).

## Repeatable checks

With an Android emulator/device connected, use Java 21 and run from `android/`:

```sh
./gradlew :app:connectedDebugAndroidTest :app:testDebugUnitTest :app:compileReleaseKotlin \
  -PlogseqRequireNativeCore=false \
  -Pandroid.testInstrumentationRunnerArguments.class=com.logseq.app.AndroidImageCompressionInstrumentedTest
```

The eleven instrumented tests exercise real Android `BitmapFactory`, `Bitmap`,
`ExifInterface`, JPEG encoding, item-provider/file persistence and FileProvider
camera output. Tests cover:

- Codec PSNR above 35 dB, reductions, small/optimized inputs and repeated preparation.
- Every EXIF orientation, 24-megapixel sampling with a 64 MiB budget, oversized
  dimension headers, a 1 MiB budget and an unwritable output directory.
- Byte-exact text/alpha PNG, real GIF/APNG/HEIC containers, opaque RAW/document data,
  MPF/XMP markers, real Android Ultra HDR gain maps and Display P3 JPEG.
- Photo/file/camera/share routes, final hashes and sizes, original provider bytes,
  MIME mismatches and unique paths for duplicate filenames.

The three small container fixtures were generated with Apple's Image I/O from a
32 × 24 sRGB bitmap: GIF/APNG have two differently colored frames; HEIC has one.
They contain no personal images. Tests do not require a Mac to load these fixtures.

## Validation evidence (2026-10-21)

Android 16 / API 36.1, arm64 emulator, Android system encoder:

| Fixture | Before (bytes) | After (bytes) | Reduction | PSNR |
| --- | ---: | ---: | ---: | ---: |
| Textured photo, 4032 × 3024 → 2560 × 1920 | 7,074,268 | 642,115 | 90.9% | 41.03 dB |
| Sonoma landscape exported to sRGB JPEG, 3840 × 2160 → 2560 × 1440 | 7,162,084 | 784,247 | 89.1% | 38.37 dB |

PSNR uses the decoded source resized by Android to the output dimensions as the
reference, separating JPEG encoding error from intentional photo downscaling.
The landscape is an optional local system-wallpaper fixture, not a bundled asset.
To reproduce that measurement, install the generated APKs, stream the JPEG into
the app's private files directory with `adb shell -T run-as com.logseq.app tee`,
then pass its absolute device path as `-e landscape PATH` to `am instrument`.
Writing an external directory as the shell user does not grant the app access.

All eleven compression tests and the separate Compose adaptive-insets test passed
together (`OK (12 tests)`). The 44 JVM tests, Release Kotlin compile and
`dune build @shared/native/runtest` also passed. No native core build is required
by this instrumented import test, and the Gradle command explicitly skips it.

---

### Adr 001 Real Time Graph Sync (`docs/adr/001-real-time-graph-sync.md`)

# ADR 001: Real-time Logseq graph sync

- Status: Accepted; implementation in progress
- Date: 2026-08-14
- Last updated: 2026-08-31
- Owners: Logseq and db-sync teams

## Context

At the time of this decision, Logseq read and wrote a small, Logseq-specific projection through the
Graph API and authenticates those requests with a personal access token (PAT). Its
local database also defines a reduced schema whose property types do not always
match the graph database on the server. Refreshing is periodic, so a change made by
another Logseq client is not visible immediately.

Before this work, the native persistence path was not a db-sync graph store. It serialized
OCaml values with `Marshal` into a two-column `kvs(address, payload)` table. db-sync
snapshots contain ClojureScript Transit values in Logseq's
`kvs(addr, content, addresses)` layout. Those formats must not be treated as
interchangeable. The earlier platform-to-core call and HTTP adapter also buffered one
complete JSON response. Incremental delivery now reuses db-sync's existing authorized
WebSocket and adds an entity-change pull message instead of another streaming endpoint.

Logseq needs a local, queryable copy of the selected graph and near-real-time
updates from db-sync. The design must work for both encrypted and unencrypted
graphs, preserve the server graph schema exactly, and add as little protocol and
server complexity as possible.

## Decision drivers

- Users sign in inside Logseq with their Cognito credentials; they do not open
  an external login page or create or paste a PAT.
- An authorized user can open any graph available to that account.
- The local graph uses the server-provided schema. Attribute cardinality, uniqueness,
  value type, reference semantics, and values are not coerced for the Logseq app.
- The first use of a graph creates a complete local copy before incremental sync
  begins.
- Incremental messages describe the latest affected entities, not DataScript tx-data.
- Incremental delivery is resumable, idempotent, ordered, and small.
- The server never receives graph decryption keys or plaintext for protected values.
- Logseq can create blocks and modify block properties, but cannot request an
  entity deletion.
- Authentication, networking, and operating-system integration are platform
  adapters; sync protocol, codecs, state transitions, encryption orchestration,
  persistence, and graph mutations are implemented in the OCaml core.

## Implementation state

This ADR defines the target architecture. The implementation is deliberately being
rolled out in stages, so target behavior must not be mistaken for current platform
support:

| Area | Current state |
| --- | --- |
| Apple authentication | Cognito Hosted UI authorization-code flow with PKCE through `ASWebAuthenticationSession`; refresh tokens are stored in Keychain |
| Android authentication | Cognito Hosted UI authorization-code flow with PKCE through Custom Tabs; refresh tokens are encrypted with an Android Keystore key |
| Graph discovery | Authenticated db-sync `GET /graphs`, including encrypted-graph metadata |
| Graph catalog and offline open | The complete discovered graph catalog is persisted in the app metadata store; the last selected graph and its local mirror open before authentication or network restore |
| Unencrypted iOS sync | iOS Simulator E2E against local db-sync verifies first-open snapshot import, server-to-client WebSocket sync, semantic REST creation, authoritative self-echo, online restart recovery, offline restart with a durable pending block, and cursor advancement after reconnect |
| Snapshot baseline | `snapshot/download` returns the pre-stream server `t`, schema version, row count, stream URL, and content encoding in one metadata response; OCaml commits that `t` only after atomic import |
| Sync ownership | One coordinator owns the selected graph's foreground WebSocket or bounded background replay, never both. Native code owns the socket lifecycle; OCaml owns Transit decoding, validation, cursor advancement, idempotency, and graph application. |
| Local metadata | Graph catalog, selection, pending writes, and checkpoints use versioned Transit records. Legacy `Marshal` metadata is intentionally treated as a cache miss; no old-cache migration is required. |
| Local journal reads | OCaml maintains an incremental recent-journal projection and re-queries only affected blocks, rebuilding only when journal-page membership changes. |
| iOS background sync | Background entry starts an immediate, bounded sync under a UIKit background assertion; `BGAppRefresh` is also registered for later catch-up. Each execution opens the persisted graph, submits pending semantic REST writes, and replays graph events from `appliedServerT` without graph-catalog or semantic refresh |
| Android sync transport | OCaml core, full-snapshot download/import, and the Android host's native WebSocket entity-change streaming are connected; device E2E remains pending |
| Encrypted graph sync | iOS Simulator E2E against local db-sync verifies encrypted first-open snapshot import, local key unlock and Keychain restore, semantic REST journal/block creation, authoritative WebSocket confirmation, ciphertext-only server storage, and kill/relaunch persistence |

## Decision

### 1. Authenticate inside the app with Amazon Cognito

Logseq uses Cognito Hosted UI instead of implementing password, challenge,
account-recovery, and federated sign-in screens. Apple opens the authorization
endpoint with `ASWebAuthenticationSession`; Android uses Custom Tabs. Both use the
authorization-code flow with S256 PKCE and a public app client without a secret.

Authentication is exposed to the shared application model through a small platform
service implemented independently by SwiftUI and Android. OAuth state and tokens do
not cross the OCaml graph boundary; the adapter supplies an access token only when
the OCaml core requests an authenticated transport operation.

The Cognito app client is a public native client with no client secret and allows
authorization-code and refresh-token grants. Cognito Hosted UI owns sign-in and
challenge presentation. Logseq persists tokens in Keychain or an Android
Keystore-backed encrypted store and refreshes them shortly before expiry. Every
Graph API, db-sync, asset, and key-management request will use:

```http
Authorization: Bearer <cognito-access-token>
```

The db-sync HTTP layer validates the JWT issuer, signature, expiry,
`token_use=access`, and Cognito `client_id`, then apply the same graph membership and
permission checks used by existing sync clients. A token identifies a user, not a graph. Graph
selection therefore happens only after the app calls the authenticated db-sync
`GET /graphs` operation. Semantic reads and writes remain under `/api/v1/graphs/...`;
graph discovery does not use the semantic graph-list route.

Authentication depends only on platform browser, secure-storage, cryptography, and
HTTP APIs. Amplify and the AWS SDK are not app dependencies.

PAT configuration and the PAT input screen will be removed after Cognito login is
available. PATs may remain supported by existing APIs during migration, but Logseq
Logseq will not use or persist them. Federated social sign-in is outside this decision
because Cognito User Pool federation normally uses a browser redirect; it must not
be added by silently changing the built-in-login requirement.

### 2. Use one server schema and keep sync metadata outside the graph

The db-sync server and Logseq local graph will use the exact schema encoded in
the snapshot root for the server-reported Logseq schema version. It must match the
corresponding `logseq.db.frontend.schema/schema` definition. Schema datoms and
property-definition entities already stored in the graph remain part of the
existing snapshot. Logseq will not maintain a reduced or Logseq-specific copy of
graph attributes.

The existing snapshot and tx-log formats use Transit. The new entity-change payload
will do the same so keywords, UUIDs, instants, integers, sets, and references retain
their logical types. References are normalized to stable lookup refs using the same
normalization rules as the current tx log; they are not changed into untyped JSON
strings. Cardinality-many values retain set semantics. The client will reject a
snapshot or change set when a value does not conform to the shared schema instead
of coercing it.

The Logseq cache is retained as a rebuildable UI projection,
but it is not the graph mirror and is never a sync source of truth. Each selected
graph currently has `graph.sqlite` for the Logseq-layout KVS graph and an atomically
replaced Transit `sync.checkpoint` sidecar containing graph id, schema version, and
applied server `t`. The separate app metadata store persists the discovered graph
catalog, last selected graph, pending local requests, and UI state. Those records
are not graph attributes and are not a second graph cursor.

Local graph files live under `graphs/<sanitized graph name>-<graph-id>/`.
Names retain Unicode characters and case; whitespace and path separators become
underscores, so directory names contain no spaces. Each graph directory contains its graph database, UI projection, checkpoint,
search index, and `Assets/` directory. Attachment metadata participates in graph
sync; attachment bytes are uploaded separately and downloaded on demand into
that graph's `Assets/`. Older ID-only and space-separated graph directories are migrated before
opening their databases. Legacy shared asset paths remain readable and are
copied into the owning graph when resolved.

Metadata records are encoded as versioned Transit. Compatibility with the earlier
`Marshal` cache is intentionally out of scope: an old record is a cache miss and is
rebuilt from the selected graph or server. This does not change the graph SQLite
layout, server schema, or the type of any graph property.

No local-only attributes such as sync status or cached JSON projections will be
added to the graph database. This prevents the Logseq app from changing the meaning
or type of any server property.

Schema changes are a synchronization boundary. A WebSocket `reset` message with reason
`schema-changed` makes the client download a new snapshot into a new local database
and atomically replace the old database after validation. The client must not try to
infer or migrate a newer server schema.

### 3. Implement the client sync engine in OCaml

All cross-platform sync behavior will be implemented once in the existing native
OCaml core. Swift and Kotlin must not contain independent implementations of the
snapshot importer, WebSocket event decoder, cursor state machine, entity merge rules, schema
validation, E2EE value transformation, optimistic reconciliation, or write
allowlist.

The OCaml core owns:

- the db-sync request and response types;
- Transit and EDN codecs;
- WebSocket event validation, replay, and server `t` state;
- framed snapshot decoding, staging, schema-first import, and atomic activation;
- the exact Logseq graph schema and typed DataScript values;
- entity upsert and remote-deletion application;
- E2EE key-state and protected-value transformation, using platform crypto
  primitives where required;
- validation that outgoing mutations are block creation or block-property updates;
- local query projections consumed by the Logseq UI.

Platform code is limited to Cognito UI and SDK calls, HTTP/WebSocket transport,
secure key and token storage, file handles, OS cryptographic primitives, background
lifecycle, and presentation. These adapters pass opaque byte chunks and explicit
results to OCaml. They do not parse or rewrite graph values.

The WebSocket adapter opens and owns the platform network task, then feeds complete,
bounded messages and terminal results into short, serialized OCaml FFI calls. It must not hold
an OCaml runtime lock for the lifetime of a connection. Blocking native REST
transport releases the OCaml runtime while waiting, so an unreachable server cannot
serialize unrelated local graph operations behind its timeout. Snapshot downloads
likewise stream to a file or bounded OCaml decoder;
they are not materialized as one JSON RPC string.

The existing JSON RPC across the Swift/Kotlin-to-OCaml FFI remains a small control
and UI envelope. Graph snapshots, WebSocket entity values, and outgoing graph mutations
do not pass through `Yojson.Basic`; keeping them as typed OCaml values avoids the
property-type loss this ADR forbids.

Both codec projects are cross-target OCaml implementations supporting native OCaml,
js_of_ocaml, and Melange; they are not Melange-only libraries. Logseq's mobile
core uses their native backends:

- `melange-transit-native` for the existing db-sync Transit JSON protocol, snapshot
  row contents, entity events, keywords, UUIDs, dates, maps, sets, and tagged values;
- `melange-edn-native` for schema or configuration artifacts that are represented as
  EDN and for typed EDN/Yojson conversion at explicit non-graph boundaries.

The shared core packages provide the common value models, and the native packages
provide native parsing and writing. The js_of_ocaml and Melange variants are not
runtime dependencies of the mobile app, but their cross-backend test suites are
useful compatibility evidence for payloads also produced by ClojureScript.

Transit remains the sync wire format. EDN is not an intermediate representation for
Transit payloads. The dependencies are pinned to exact Git revisions by the native
dependency build script so Apple and Android builds use the same codec version. Cross-codec
fixtures copied from `logseq-1` must prove byte-level decoding compatibility before
the old JSON projection path is removed.

`datascript-ocaml` already uses `melange-transit-native` in its native SQLite work
and contains `Logseq_sqlite_storage`, which recognizes Logseq's Transit root, tail,
and persistent-sorted-set nodes and has tests against generated and real Logseq
graphs. That implementation will be promoted from its example library into a
supported package and reused. The existing `datascript-ocaml-native.sqlite` adapter
and Logseq's `Marshal` codec cannot open a db-sync snapshot directly and will
not be used to decode it.

### 4. Bootstrap with the existing db-sync snapshot download

When a user opens a graph that has no completed local copy for its graph id and
schema version, the client will follow the existing db-sync snapshot contract used
by Logseq:

1. `GET /sync/:graph-id/snapshot/download` to obtain the existing snapshot stream
   URL together with the current server transaction number `t`, schema version,
   row count, and content encoding. That `t` is the pre-stream replay baseline.
2. Download the gzip-capable framed Transit stream of SQLite `kvs` rows
   `[addr, content, addresses]`.
3. Stage those rows unchanged in a temporary Logseq-layout SQLite database.
4. Use the promoted native `Logseq_sqlite_storage` Transit decoder to load the
   snapshot schema and datoms, then reconstruct the app's local DataScript graph in
   schema-first import order, as the current `frontend.worker.sync.download`
   implementation does.

The platform adapter may inspect the stream URL and content encoding to perform the
download, but passes the original metadata response to OCaml. The OCaml sync session
validates `t`, schema version, and row count and owns the import checkpoint.

No new snapshot envelope or parallel Graph API snapshot route will be introduced.
The existing snapshot preserves all `kvs` graph storage; both server and client open
it with the same version of `db-schema/schema`. Asset bytes remain in the existing
asset API; asset entities and metadata are in the graph snapshot.

The snapshot-download handler reads baseline `t` before the client consumes the
separate snapshot stream. After import, WebSocket replay starts from that `t`, so every
transaction concurrent with snapshot streaming is considered again.
Complete entity upserts are idempotent, making an entity already present in the
snapshot safe to replay. db-sync must not garbage-collect tx-log entries newer than
the baseline while the download is active; if it cannot serve the range, it sends a
reset and the client downloads again.

The client verifies the stream framing and imports into a new local database. It
records the baseline `t` only after import succeeds, then exposes the graph. An
interrupted or invalid import is discarded and restarted; a partial graph is never
opened.

### 5. Support encrypted and unencrypted graphs with the same sync protocol

The existing graph-list metadata identifies whether a graph is encrypted. For an
encrypted graph, Logseq will use the existing Logseq E2EE key flow:

1. Fetch the user's encrypted private key with the Cognito access token.
2. Unlock it locally with the user's E2EE password.
3. Fetch the graph AES key encrypted for that user.
4. Decrypt protected attribute values and encrypted assets only on the device.

The server stores and transmits protected values as ciphertext and does not receive
the password, private key, or plaintext AES key. Unprotected schema and identity
fields remain available to db-sync so it can authorize access, identify affected
entities, and produce change sets.

The existing snapshot format and the new entity-change format each work for both
encrypted and unencrypted graphs. For encrypted graphs, the local DataScript graph
is an exact ciphertext mirror of the server graph. Protected datoms are not replaced
with plaintext during snapshot import or WebSocket event application. Local queries build a
separate decrypted UI projection on demand. This avoids a second persisted graph
shape and ensures restart, replay, and export preserve the same values the server
stores. Existing Logseq encryption metadata, Transit value envelopes, and algorithms
are reused rather than introducing a Logseq-specific format. Non-protected values keep
their original DataScript types; encrypted values keep the server's existing E2EE
representation unchanged in the mirror.

If the key is missing, access was revoked, or decryption fails, the app stops import
or event application and locks the graph. It does not store a partly decrypted graph
or fall back to treating ciphertext as user content.

### 6. Stream entity change sets over the existing WebSocket

db-sync exposes one authorized WebSocket for the selected graph:

```http
GET /sync/:graph-id
Upgrade: websocket
Authorization: Bearer <cognito-access-token>
```

After connecting, Logseq requests entity changes from its last committed cursor:

```json
{"type":"entity/pull","since":48192}
```

The server already broadcasts `changed` messages containing the latest server `t`.
The client coalesces those notifications and sends another `entity/pull` only when no
pull is in flight. On token expiry or disconnection, it obtains a current token and
reconnects with bounded exponential backoff from its durable cursor.

Normal responses use the existing monotonically increasing, graph-branch-scoped
server transaction number as the cursor:

```json
{"type":"graph-changes","data":"<Transit payload>"}
```

The actual `data` payload is Transit-encoded even when shown as JSON above. It has
only these change collections:

- `upserts`: the complete latest server representation of each affected entity;
- `deleted`: stable remote entity identifiers for entities that no longer exist.

An entity identifier is a typed lookup identity, not a numeric DataScript entity
id. The initial protocol supports `[:block/uuid <uuid>]` and `:db/ident` identities,
which are exactly the identities retained by the current normalized tx log. An
upsert is encoded as that identity plus a complete attribute map. Every reference
value is encoded as a typed lookup identity as well; no server-local numeric entity
id crosses the wire.

The implementation reuses `storage/fetch-tx-since`, `storage/get-t`, and the current
DataScript connection in the per-graph Durable Object. For `t-before`, the handler
captures the current immutable database value and `t-now`, reads normalized tx-log
entries in `(t-before, t-now]`, and extracts stable entity identities from their
entity positions, `:block/uuid` additions, and `:db/retractEntity` lookup refs. It
deduplicates those identities and reads their current datoms from the captured
database, converting reference entity ids back to lookup identities according to
the exact schema.
Each surviving identity becomes one latest-state upsert; each affected identity
absent from that database becomes a deletion. The event advances directly to
`t-now` because the protocol promises the latest state, not every intermediate
transaction.

The current normalizer does not retain a stable identity for every possible
DataScript entity (for example, a file entity identified only by `:file/path`). The
server must not emit a partial entity or numeric id in that case. Before enabling
WebSocket entity replay for such entities, normalization is extended to retain every supported unique
identity and the server capability's minimum cursor is advanced. A replay that
encounters an older unidentifiable transaction returns `reset` and requires a fresh
snapshot. This keeps legacy tx-log limitations explicit instead of silently losing
changes.

For a live commit, the transaction listener can perform the same derivation from
the transaction report's tx-data and immutable `db-after`, paired with the existing
server transaction number. Capturing the database value and `t-now` under the
graph's serialized writer prevents a transaction from being present in one and
absent from the other.

Raw datoms and tx-data remain an internal change index and are never sent to Logseq
Logseq. Multiple writes to an entity in the range produce one final upsert. If an
entity was changed and then deleted, only its id is included in `deleted`. No
unchanged entity is included. Transactions that affect no graph entity can advance
`t` with an empty change set so continuity is preserved.

An upsert is a complete entity, rather than a partial property patch. This is the
smallest idempotent unit that also communicates property retractions without
recreating tx-data semantics. Applying the same event more than once has no effect:
the client replaces the known attributes of every upserted entity, retracts local
attributes absent from that representation, and retracts every deleted entity.

The client applies one event and advances its local `t` in one durable operation.
It must reject an event when its graph id or schema version differs, or when
`t-before` does not equal the last committed `t`.

If the requested cursor has expired or is ahead of a reset server, the schema changed,
a message is too large, or the server cannot prove a contiguous range, the server sends:

```json
{"type":"reset","data":"<Transit reset payload>"}
```

Reset-style snapshot replacement closes existing sockets so they reconnect against
the new graph generation. The client then closes the socket and repeats the full-snapshot process. Reconnect
replay is at least once; the cursor and idempotent entity upserts make duplicates safe.

### 7. Restrict client writes to block creation and property modification

Logseq may submit only these mutations through authenticated server APIs:

- add a new block;
- modify properties of an existing block.

Writes use the existing semantic REST resources under `/api/v1/graphs/:graph-id`.
Block creation uses `capture` (and the corresponding task or asset operation), and
property changes use the block property update resource. Logseq does not call
the db-sync tx-batch endpoint and does not submit raw DataScript transactions.

The client will not expose or call entity-delete, page-delete, block-delete,
retract-entity, arbitrary transaction, move, or asset-delete operations. The server
must enforce this allowlist; hiding controls in the UI is not sufficient. For an
encrypted graph, the client encrypts protected property values with the graph AES
key before upload, using the existing E2EE codec.

If a journal page for a pending encrypted create does not yet exist, the client first
calls the existing semantic page-create resource. It supplies Logseq's deterministic
journal UUID, encrypted `:block/title` and `:block/name` values, and the integer
`:block/journal-day`. The server creates only that explicitly described journal page,
keeps the existing attribute types, and treats a retry with the same identity as
idempotent. The pending block is then sent through capture, task, or asset REST using
that page UUID. This avoids both raw tx submission and server-side plaintext
generation.

This restriction applies to mutations initiated by Logseq. The local database
is still a mirror, so it must apply deletions received over WebSocket sync when another
authorized Logseq client or server process deletes an entity. A remote deletion is
never converted into or replayed as a client-originated delete request.

A successful create or property update is not considered synchronized merely
because its HTTP request succeeded. The originating client receives the same
authoritative entity upsert through WebSocket sync as other clients. Optimistic UI state may be
shown separately, but only the authoritative sync event advances the graph cursor and replaces the
local graph value. Existing server conflict and permission rules remain
authoritative.

Every supported mutation is local-first. The OCaml core commits it to the app's
durable pending store before the UI publishes the result. A single serialized sync
pump sends queued operations when transport is available. An HTTP failure
leaves the operation retryable; it must not roll back the local edit, hide the graph,
or block subsequent create, edit, search, and navigation operations. A remote read
or refresh cannot overwrite an unsynchronized local value. On successful REST
submission the operation enters a submitted state and remains overlaid on the graph
mirror until the matching WebSocket upsert confirms the authoritative value.

### 8. Use cursor replay during iOS background execution

iOS does not guarantee a continuously running network connection after the app is
backgrounded. The app therefore keeps the long-lived WebSocket task for foreground use,
starts an immediate bounded sync under a UIKit background assertion when the scene
enters the background, and registers a `BGAppRefresh` task as an opportunistic later
catch-up mechanism. The refresh task is registered and initially scheduled during app
launch, then rescheduled whenever the scene enters the background and whenever a
background execution begins.

During each execution window the app:

1. opens the app metadata database and the last selected graph's local SQLite mirror;
2. obtains a current Cognito access token, refreshing it through the OAuth token
   endpoint when necessary;
3. restores the persisted graph catalog, selection, schema, and `appliedServerT`
   checkpoint without refreshing the graph catalog or downloading a new snapshot;
4. submits durable pending writes through the semantic REST allowlist;
5. opens the graph WebSocket and sends `entity/pull` from `appliedServerT`, consumes
   the first complete response, applies the available latest entity changes, commits
   the new cursor atomically, and closes the socket rather than holding background execution open.

The background path never calls the semantic recent-block refresh API. If the graph
has no valid local baseline, the task exits and leaves first-open snapshot download to
foreground execution. If an encrypted graph cannot restore its cached key, the task
leaves its pending operations intact and exits without applying ciphertext as user
content. `BGAppRefresh` timing remains controlled by iOS; strict push-time background
delivery would require a later APNs silent-push capability and is not part of this ADR.

## End-to-end flow

1. Log in through Cognito Hosted UI using authorization code with S256 PKCE.
2. List authorized graphs and select one.
3. Resolve the graph's schema version and encryption flag.
4. If encrypted, unlock the graph key locally.
5. If no valid local copy exists, call the existing `snapshot/download` API, retain
   its metadata `t` as the pre-stream baseline, and atomically import its framed
   `kvs` row stream.
6. Open the graph WebSocket and request changes from baseline server transaction number `t`.
7. Apply ordered entity upserts and deletions transactionally and persist each
   cursor.
8. On disconnect, refresh the Cognito session if necessary and resume from the
   committed cursor.
9. On `reset`, build a new local database from a fresh full snapshot.
10. When iOS grants background time, reopen the last graph, submit pending writes,
    and run one cursor-based event replay without semantic polling.

## Required server changes

- Reuse the existing Cognito JWT verification and graph access checks, and add the
  Logseq Cognito app client id to the allowed client ids.
- Expose graph encryption and schema-version metadata in the graph list.
- Keep `GET /sync/:graph-id/snapshot/download` and its framed Transit `kvs` stream as
  the only full-download path. Its metadata response includes the pre-stream `t`,
  schema version, row count, stream URL, and content encoding; bootstrap does not
  add a separate `/pull` request.
- Extend the existing `/sync/:graph-id` WebSocket with `entity/pull`,
  `graph-changes`, and `reset` messages; do not add a parallel event endpoint.
- Use `storage/fetch-tx-since` and `storage/get-t` to find affected stable entity
  identities, then pull their latest values from the current DataScript connection;
  do not introduce a second change database, cursor, or an `as-of` query.
- Use the Cognito `client_id` claim to enforce that Logseq may call only
  block-create and block-property-update mutations.
- Allow encrypted semantic writes only for the explicit Logseq operations. Encrypted
  journal creation requires a deterministic UUID, encrypted title and name, and the
  existing integer journal-day attribute; it does not accept arbitrary tx-data.
- Emit metrics for connected streams, replay lag, event size, reset reason,
  authorization failure, and snapshot duration.

## Required Logseq changes

- Replace PAT configuration with Cognito Hosted UI login, OAuth refresh and logout,
  and platform secure storage.
- Add pinned `melange-transit-native` and `melange-edn-native` dependencies to the
  OCaml build.
- Promote and reuse `datascript-ocaml`'s tested Logseq KVS Transit reader; do not
  feed server snapshot rows to the historical `Marshal`-based SQLite adapter.
- Implement protocol codecs, snapshot import, WebSocket sync state, entity application, E2EE
  orchestration, and mutation validation as OCaml modules behind narrow signatures.
- Keep Swift and Kotlin adapters byte-oriented and free of graph-schema or sync-state
  logic.
- Add graph selection and encrypted-graph unlock states.
- Replace the reduced local schema with the exact `logseq-1` graph schema for the
  server-reported schema version and reuse the current snapshot row import design.
- Store the applied server `t` and snapshot import state outside the graph database.
- Persist the full graph discovery catalog and last selected graph in the app
  metadata store so an existing graph opens without authentication or network.
- Use the platform WebSocket client with reconnect, replay, atomic event application, and reset
  handling.
- Add byte-stream platform adapters and short serialized FFI entry points so the
  current blocking JSON RPC mutex is never held by a live stream.
- Query the local full graph for Logseq views and expose only add-block and
  modify-block-properties mutations.
- Persist every mutation before presentation, send queued writes only through the
  semantic REST API, and retain optimistic values until authoritative WebSocket echo.
- Reuse Logseq's E2EE key and value codecs, persist the exact ciphertext mirror, and
  expose plaintext only through an in-memory decrypted query/UI projection.
- Register and schedule iOS background refresh, restore the last selected local
  graph, submit pending writes, and replay graph events from the durable cursor
  without calling semantic refresh.

## Consequences

### Positive

- The Logseq app sees changes shortly after they commit without periodic full refresh.
- Full local data enables offline and low-latency queries.
- Reusing db-sync snapshot download and the DataScript tx-data log avoids a second
  snapshot or change-history subsystem.
- One schema and typed transport prevent subtle cross-client property corruption.
- Entity snapshots are simpler and safer to apply than raw transaction history.
- Reusing db-sync's existing authenticated WebSocket avoids a second long-lived
  transport and its duplicate connection management.
- The same protocol supports encrypted graphs without weakening E2EE.
- One OCaml sync engine gives Apple and Android identical cursor, codec, schema,
  encryption, and mutation behavior.

### Negative

- A first open can be expensive for a large graph.
- Complete affected entities may repeat unchanged attributes, although only affected
  entities are sent.
- The server must retain enough cursor history for useful reconnects or force a new
  snapshot.
- Mobile lifecycle limits mean real-time delivery is guaranteed only while the app
  can maintain the stream; cursor-based `BGAppRefresh` remains a best-effort fallback
  whose timing is controlled by iOS.
- E2EE password and key recovery add user-visible failure states.
- Built-in Cognito authentication requires native UI for all enabled challenge
  states on both Apple and Android platforms.
- The native OCaml build gains two codec dependencies and requires compatibility
  fixtures against the ClojureScript server formats.

## Rejected alternatives

### Continue polling the Graph API

Polling is simple but wastes requests, increases update latency, and cannot provide
a reliable contiguous cursor.

### Embed Cognito SDK authentication screens

Embedding separate native authentication screens duplicates Cognito challenge and
recovery behavior and adds a large SDK dependency. Hosted UI keeps those flows in
Cognito while the app retains a small OAuth adapter.

### Add a new full-snapshot endpoint

db-sync already has `GET /sync/:graph-id/snapshot/download` and a streaming snapshot
of its DataScript SQLite `kvs` rows. Creating another endpoint or entity snapshot
format would duplicate its permission, snapshot, encryption, and import behavior.

### Send DataScript tx-data over WebSocket

Raw tx-data exposes storage internals, is difficult to version, can contain
intermediate values, and forces every client to reproduce server transaction
semantics. It also makes schema and encryption mistakes more likely.

### Send partial entity patches

Patches are smaller in some cases but require explicit attribute-retraction and
cardinality-many merge rules. Those rules become another form of tx-data. Complete
latest entities are idempotent and unambiguous.

### Maintain a Logseq-specific graph schema

A projection is attractive for UI code but changes types, drops data needed by
references, and creates a second schema that must migrate independently. UI models
may project from the local graph in memory, but the stored graph must use the server
schema.

### Implement sync separately in Swift and Kotlin

Separate platform implementations would duplicate the most correctness-sensitive
state machine and make schema, encryption, and replay behavior diverge. Platform
code provides capabilities to the OCaml core instead.

### Add a separate downstream streaming endpoint

A second streaming or polling endpoint would duplicate authorization, connection lifecycle,
and reconnect behavior. The existing graph WebSocket already provides authenticated
change notifications, so entity replay belongs on that connection.

## Rollout and verification

The snapshot and WebSocket entity payloads are versioned independently from the graph schema.
The new path will be enabled behind a server capability flag and rolled out first to
test graphs, then unencrypted graphs, then encrypted graphs.

The implementation is complete when automated integration tests demonstrate:

- built-in Cognito login, challenge handling, session refresh, logout, expired-token
  reconnect, and graph authorization;
- exact schema and value-type parity after a full snapshot import;
- native OCaml Transit fixtures decode snapshot rows and WebSocket entities produced by
  `logseq-1`, then encode values the server decodes without type changes;
- native OCaml EDN fixtures preserve every schema/configuration value used by the
  app, including UUID and instant tags;
- the same OCaml sync tests run for both Apple and Android core builds, with no sync
  decisions implemented in Swift or Kotlin;
- first-open behavior never exposes a partial graph;
- remote create, update, property retraction, reference change, move, and entity
  deletion produce minimal, latest-state events with no tx-data on the wire;
- the server derives those events solely from the existing DataScript tx-data log,
  existing server transaction numbers, and a captured current database value;
- Logseq can add blocks and modify block properties, while all delete and
  arbitrary transaction requests are rejected by the server;
- duplicate and replayed events are idempotent and cursor gaps trigger reset;
- disconnect/reconnect does not lose committed changes;
- while the API is unreachable, repeated creates and property edits remain
  responsive, survive process termination, preserve their latest state, and synchronize after
  reconnect without duplicate server entities;
- schema changes force atomic re-bootstrap;
- encrypted and unencrypted snapshots converge to the same logical graph data;
- encrypted changes and assets are never plaintext in server logs or wire captures;
- revoked graph access terminates the stream and prevents reconnect;
- iOS background execution never performs semantic polling, preserves pending writes
  when offline or locked, and resumes entity replay from the last committed server
  transaction number.

## References

- `logseq-1/docs/agent-guide/db-sync/protocol.md`
- `logseq-1/deps/db-sync/src/logseq/db_sync/storage.cljs`
- `logseq-1/deps/db-sync/src/logseq/db_sync/worker/handler/sync.cljs`
- `logseq-1/src/main/frontend/worker/sync/download.cljs`
- `datascript-ocaml/examples/logseq_sqlite_storage.ml`
- `logseq/shared/native/logseq_sqlite.ml`
- `logseq/shared/native/logseq_core_ffi.c`
- [melange-transit: native OCaml, js_of_ocaml, and Melange Transit JSON](https://github.com/logseq/melange-transit)
- [melange-edn: native OCaml, js_of_ocaml, and Melange EDN](https://github.com/logseq/melange-edn)
- [Amazon Cognito authorization endpoint](https://docs.aws.amazon.com/cognito/latest/developerguide/authorization-endpoint.html)
- [Amazon Cognito token endpoint](https://docs.aws.amazon.com/cognito/latest/developerguide/token-endpoint.html)
- [Amazon Cognito PKCE](https://docs.aws.amazon.com/cognito/latest/developerguide/using-pkce-in-authorization-code.html)
- [Amazon Cognito User Pool authentication flows](https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-authentication-flow-methods.html)

---

### Adr 002 Mobile Outliner Mode (`docs/adr/002-mobile-outliner-mode.md`)

# ADR 002: Mobile outliner mode and safe block operations

- Status: Proposed
- Date: 2026-08-16
- Owners: Logseq and db-sync teams
- Amends: ADR 001, section 3 (local pending persistence) and section 7 (client write restrictions)

## Context

Logseq currently presents journal and page blocks as Logseq-oriented cards. Mobile users also need
an outliner view that follows the core interaction model of the Logseq mobile app: page or journal
headings, ordered nested blocks, bullets, task states, disclosure controls, and direct block editing.
The reference is the Logseq mobile journal layout supplied for this work on 2026-08-16.

The app already has the data needed to render a tree: each projected block has a stable UUID, page
UUID, optional parent UUID, and Logseq order key. It also already supports title and task-status
updates and root-level capture. It does not yet expose structural outliner mutations or client-originated
deletion. ADR 001 deliberately prohibited move and delete requests while the synchronization path was
being established.

A mobile outliner is not useful if it merely resembles an outline but cannot safely perform the small
set of structural operations users need while editing. At the same time, Logseq should not port
the complete desktop editor, command system, plugin API, selection model, or undo engine. The design
must therefore define a minimal mobile operation set and one safe mutation path shared by Logseq and
Outliner modes.

## Decision drivers

- Journals can switch between Logseq and Outliner modes from an icon button in the header. Regular pages
  are Outliner-only and never show Logseq mode or the Logseq composer.
- Outliner ordering and nesting match the selected local Logseq graph rather than a second UI-owned
  copy of the tree.
- Logseq and Outliner modes use the same mutation commands and safety checks.
- Ordinary block deletion is supported in both modes through Logseq's `delete-blocks` outliner
  operation. Recycle remains page-only and is not used for blocks.
- Structural edits remain local-first, durable, idempotent, and compatible with WebSocket reconciliation.
- The server remains authoritative for Logseq order generation and graph invariants.
- Encrypted and unencrypted graphs use the same semantic operation envelope.
- The implementation covers mobile editing needs only. Undo and redo are explicitly excluded.
- The OCaml core owns operation validation, optimistic projection, pending state, and reconciliation;
  Swift and Kotlin only express UI intent and platform presentation.

## Decision

### 0. Use a functional core with an Elm-style update loop

The reusable outliner is an OCaml feature module, not a SwiftUI feature model. Its boundary is:

```text
Context + State + Message -> State + Command list
Command list + injected time/UUID providers -> semantic operations + platform commands
```

`update` is pure and owns editing drafts, UTF-16 caret state, selection, autocomplete, collapse,
zoom, keyboard semantics, structural planning, cycle checks, and drag/drop placement. The command
interpreter is the only layer that creates operation IDs, reads time, allocates block UUIDs, or stages
pending graph operations. Those dependencies are injected so reducer and interpreter tests are fully
deterministic.

SwiftUI renders the returned snapshot, sends UI events, and executes only platform commands such as
haptic feedback, focus, clipboard access, file picking, and a destructive confirmation dialog. It
does not calculate fractional order keys, split strings, merge titles, derive autocomplete results,
validate moves, or keep a second editing/selection/tree state. Events are serialized through the core
executor so text and caret changes cannot overtake Return, Backspace, or toolbar actions. A future web
shell can use the same reducer, selectors, command interpreter, projection, and synchronization code.

### 1. Add a content mode, not a second navigation destination

Journals have one content surface with two presentation modes:

- `Logseq`: the existing chronological card presentation and composer;
- `outliner`: a Logseq-style hierarchical block presentation.

An icon-only button in the existing header toggles the mode. It uses a bundled vector-drawn icon,
has the accessibility identifier `button.content-mode`, and announces the destination mode, for
example, "Show outliner" or "Show Logseq". The selected mode is app-local presentation state. It is not
written into the graph or sent to the server.

The last selected journal mode is persisted locally. Logseq remains the default for existing
installations until the user switches. Regular pages always render the Outliner and hide the mode
button and Logseq composer. Search results and the graph picker do not expose a mode switch.

Switching mode does not change the selected graph, page, search query, block selection, sync cursor,
or graph data. It only changes how the current block projection is rendered.

### 2. Build the visible outline from existing graph fields in OCaml

For each journal section or selected page, the OCaml selector builds a forest from:

- `uuid` for stable identity;
- `pageId` for the owning page;
- `parentId` for nesting;
- `order`, with `createdAt` and UUID as deterministic fallbacks;
- title, task status, references, and sync state for row content.

The snapshot exposes core-owned outliner state and derived rows. Each row contains the block, depth,
whether it has children, and its collapsed state. A parent outside the current section is treated as
the page root.
Cycles and orphaned parent references must not hide data: traversal uses a visited UUID set and appends
unvisited blocks deterministically at root depth. The renderer never recursively traverses without a
cycle guard.

Collapse, expand, and zoom are reducer-owned presentation operations in this version. They do not
update `:block/collapsed?`, enqueue a graph
write, or affect another Logseq client. Persisted graph collapse state can be added later without
changing the tree model.

### 3. Match the essential Logseq mobile outline presentation

Outliner mode renders:

- a journal or page heading above its blocks;
- one row per visible block in Logseq order;
- indentation derived from tree depth;
- a tappable bullet at the leading edge that zooms into that block subtree;
- a disclosure affordance at the trailing edge only when the block has children;
- subtle ancestor guide lines for nested content;
- task completion and cancellation styling without changing the stored title;
- existing reference/link rendering where the shared renderer supports it;
- existing pending and failed-sync feedback without turning it into graph data.

The supplied screenshot is a visual direction, not a requirement to reproduce unrelated Logseq mobile
navigation, author metadata, bottom tabs, or menus. Logseq must not fabricate author information
that is absent from its projection.

Tapping a title enters the inline Outliner editor; it never opens the Logseq composer. Tapping a task
marker uses the shared task-status operation. Tapping a disclosure control changes only local
collapsed state. A long press enters block-selection mode, and subsequent taps extend or reduce a
multiple selection. Buttons, toolbar commands, zoom, collapse/expand, and selection activation emit
native iOS haptic feedback. Logseq cards retain their existing composer editing and destructive delete
command.

### 4. Support only the mobile outliner operation set

The following operation set is the product contract. UI gestures and keyboard behavior may be
different representations of the same semantic operation; they must not create additional mutation
types.

| Operation | Mobile behavior | Graph mutation | Initial UI |
| --- | --- | --- | --- |
| `save-block-title` | Edit and save one block title | Update the existing block | Inline/shared editor |
| `set-task-status` | Set or cycle a task status | Update the status property | Task marker/status picker |
| `insert-block` | Insert an empty or titled sibling/child at a semantic position | Create one block | Return/new-block action and action menu |
| `split-block` | Split title at the caret and insert the suffix as the next block | Atomic title update plus insert | Return in the editor |
| `merge-backward` | Merge a block into its previous visible block at title start | Atomic title update, child move, and deletion of the emptied block | Backspace at title start |
| `move-block` | Move before, after, or inside another block | Update parent, page, and order atomically | Indent, outdent, move up/down, and drag/drop |
| `delete-block-subtree` | Delete a block and its descendants after confirmation | Logseq `delete-blocks` outliner operation | Logseq card and Outliner action menu |
| `collapse-block` / `expand-block` | Hide or reveal descendants | None in this version | Disclosure control |

`indent`, `outdent`, `move-up`, and `move-down` are not separate graph mutations. The OCaml reducer
resolves selected roots and intended neighbors against the projected DB, while its command interpreter
allocates compatible fractional order keys and emits one atomic `move-blocks` intent. Similarly,
`split-block` and `merge-backward` are named atomic editor
intents because exposing their constituent writes separately could leave the graph in an intermediate
or destructive state.

The implementation supports long-press multiple selection for the safe batch commands in the
selection toolbar. Structural batch commands validate the top-level selected roots, preserve their
relative outline order, and reject cycles or ambiguous cross-page placements. Drag/drop uses the same
`move-block` semantic command as indent/outdent; it is not a UI-owned tree rewrite.

The implementation exposes `save-block-title`, `set-task-status`, `insert-block`, atomic
`split-block`, atomic `merge-backward`, `move-block`, `delete-block-subtree`, and presentation-only
collapse/expand. Undo and redo remain excluded. Split and merge must not be exposed until the server
accepts each intent atomically with an operation identity and authoritative cursor guard.

The operation algebra is a mobile allowlist over Logseq's existing semantic outliner ops, not a new
tree model:

| Mobile intent | Logseq semantic foundation | Mobile restriction |
| --- | --- | --- |
| `save-block-title` | `:save-block` | Stable UUID plus parsed title fields only |
| `set-task-status` | `:set-block-property` for `:logseq.property/status` | Existing status choice only; associated Task tag semantics are preserved |
| `insert-block` | `:insert-blocks` | One block, stable client UUID, same page |
| `move-block` | `:move-blocks` | One source and stable target UUID; only before/after/first-child/last-child placements |
| move up/down, indent/outdent gesture | Logseq `:move-blocks-up-down` or `:indent-outdent-blocks`, canonicalized to `:move-blocks` | Canonicalization uses the projected DB and stores stable UUID refs, never numeric entity IDs |
| `delete-block-subtree` | `:delete-blocks` | One ordinary root; page and built-in entities rejected |
| `split-block` | Atomic group of `:save-block` and `:insert-blocks` | Not exposed until the server accepts the group atomically |
| `merge-backward` | Atomic group of save, child move, and `:delete-blocks` | Not exposed until the server accepts the group atomically |
| collapse/expand | Logseq has `:collapse-expand-blocks`, but mobile v1 keeps it presentation-only | No graph op or pending row |

The closed payload schemas and canonical stable-reference rules follow
`logseq.outliner.op/op-schema` and `logseq.outliner.op.construct`; only the rows above are accepted.
Options outside the required subset are rejected. In particular, the mobile client does not expose
`:transact`, page lifecycle, templates, batch operations, restore, permanent recycle deletion, or
reactions. Undo-oriented inverse-op construction is not ported.

### 5. Reuse the existing semantic endpoints

Swift dispatches typed UI messages to the OCaml update loop. The reducer and command interpreter
validate block identity and operation-specific fields, then use the existing authenticated db-sync semantic route for block
updates. Existing insertion and move routes remain the intended transport for the later UI slice.
Delete minimally extends the existing block DELETE route with the guard described below; an
unguarded DELETE is not safe for this client. The core never accepts or forwards arbitrary DataScript
transactions, and the platforms never calculate raw Logseq order keys.

The existing serialized pending-sync pump remains the single transport path. The optimistic projection
is derived from pending intent plus the local graph mirror and does not advance `appliedServerT`.
Authoritative WebSocket changes reconcile the projection. This keeps the first implementation small: it does
not introduce a second server operation protocol where an existing semantic route already expresses
the required operation.

The local graph DataScript database is strictly the authoritative mirror at `appliedServerT`. Accepted
local intents are persisted in an app-owned SQLite operation log, never transacted into graph
DataScript. All user-facing reads use this pipeline:

```text
authoritative DataScript db value + ordered pending ops
  -> semantic op-to-tx compiler
  -> repeated DataScript d/with
  -> immutable projected DataScript db value
  -> every product read
```

The core keeps two distinct DataScript values. `authoritative-db` is restored from and persisted to the
Logseq `kvs` table and is changed only by snapshot/WebSocket sync. `projected-db` is an immutable in-memory result
of applying pending projection transactions with `d/with`; it is never installed as the authoritative
connection and is never persisted. Thus pending state is represented in one complete DataScript DB for
reads without becoming server-confirmed graph state.

There is no feature-specific merge layer. Page/journal reads, linked references, object lookup,
properties/classes, tasks, arbitrary supported Datalog queries, query blocks, and outliner traversal
all receive a `read-snapshot` containing the same `projected-db`. Code that reads the authoritative DB
must be limited to sync application, conflict guards, and projection construction. A new read feature
is pending-aware by default because the read API does not expose the authoritative connection.

Supported product queries read current graph state. Transaction-history queries and code that depends
on projected transaction IDs or the projected basis value are not product APIs because rebuilding via
`d/with` may allocate different local transaction/entity IDs. UUIDs and idents remain the only stable
identities exposed outside the read layer.

The semantic op-to-tx compiler is the only place that understands pending operation effects. It
compiles operations in SQLite `sequence` order against the DB value produced by the previous operation:

- create/insert adds the stable block UUID, page, parent, provisional order, title, and derived datoms;
- save-title uses the same title parser/semantic reducer as authoritative writes, including refs, tags,
  properties, and query definitions, rather than changing only `:block/title`;
- property/task changes update both the property value and any derived object relationships;
- move changes parent/page/provisional order and propagates page membership to the moved subtree;
- delete retracts the ordinary-block subtree only from `projected-db`;
- collapse/expand remains UI state and produces no graph transaction.

Implicit supporting entities created by Logseq title semantics, such as the target of a newly typed
page reference, are part of the save-title projection even though explicit page creation UI is out of
scope. Otherwise pending linked refs and Objects would disagree with the eventual server result.

For encrypted graphs, the compiler uses the existing local E2EE transformation before `d/with`.
Protected datoms in `projected-db` keep the same ciphertext representation as `authoritative-db`, while
refs and other derived structural datoms are computed locally from decrypted editor input. Protected
plaintext is not persisted in the graph KVS or operation payload.

This is required for transitive features. For example, a pending title containing `[[Project]]` must
immediately appear in Project's linked references; a pending tag/property must affect Objects; a
pending query edit must re-evaluate against the same projected DB; and a pending delete must disappear
from all of those results without each feature implementing deletion filtering.

### Render and navigate DB-graph nodes

DB graphs use one node-reference syntax: `[[target]]`. The target can resolve through `:block/refs`
to either a page or an ordinary block. The mobile implementation does not parse, emit, autocomplete,
copy, or preserve the removed `((uuid))` block-reference syntax. Persisted references use the canonical
`[[uuid]]` identity form; presentation resolves the target title from the same projected DataScript DB.
Tags use the corresponding DB-graph identity form `#[[uuid]]` and `:block/tags` semantics.

The OCaml core invokes mldoc's inline parser and publishes a small typed render tree. Swift renders
that tree and sends typed tap actions; it does not parse Markdown or infer reference targets. Editing
always shows the raw title so formatting cannot change the source text or caret. Non-editing blocks
render the mldoc tree, including supported emphasis, code, links, `[[...]]` node references, and tags.

Page, ordinary block, tag, property, and class entities all use the same native node route. Their
entity kind selects the node page sections; it does not select a different routing mechanism. A page
node shows its blocks as an outliner, an ordinary block node shows that block as the outliner root,
and a tag node shows Tagged nodes queried from `projected-db`. Each route owns an independent core
projection keyed by node UUID, so pushing or popping a node cannot mutate the projection rendered by
the route underneath it. Native back and interactive-pop exit editing and selection before the
destination changes. Swift owns only the NavigationStack path and native presentation.

### Bound journal projection

`LazyVStack` alone is insufficient because serializing every journal and block still makes startup
linear in the whole graph. The shared core therefore owns a bounded recent-journal window and queries
only those journal pages and their visible outliner rows from `projected-db`. Reaching the oldest
loaded section sends `load-older-journals`, which expands the window by a fixed page batch. Swift does
not cache a second journal tree. Window expansion preserves the current immutable read snapshot and
is covered for thousands of journals, pending operations, collapse state, and zoom destinations.

The minimum projection contract is defined by Logseq DB-graph attributes, not by the fields currently
rendered in Logseq:

| Read behavior | Datoms that semantic projection must keep coherent |
| --- | --- |
| Page/journal and outliner tree | `:block/uuid`, `:block/page`, `:block/parent`, `:block/order`, `:block/title`, timestamps, `:block/journal-day` |
| Linked references | `:block/refs` plus referenced page/block identity (`:block/uuid`, `:block/name`, `:block/title`) |
| Tags, classes, and Objects | cardinality-many `:block/tags`, `:block/link`, class/tag `:db/ident`, and referenced entity identity |
| Properties and property queries | explicit built-in `:logseq.property/*` and dynamically discovered `:user.property/*` idents with their real scalar/ref cardinality |
| Aliases and navigation | cardinality-many `:block/alias`, `:block/name`, `:block/link`, and `:block/refs` |
| Query blocks and general current-state Datalog | the complete semantic delta produced by the operation, not a UI-specific subset |

Save-title projection retracts stale derived refs/tags/property datoms before adding the newly parsed
ones. It must not hardcode a list of user properties because DB graphs store them as dynamic
`:user.property/<name>` attributes and property values may be scalar, ref, or cardinality-many. Delete
projection uses Logseq's complete ordinary-block delete delta, including subtree and inbound/derived
reference cleanup; a bare `:db/retractEntity` of only the selected block is insufficient for linked
refs and arbitrary queries.

The projection makes pending creates, title/status/property edits, and moves immediately editable and
visible, and hides pending deletes. Search is evaluated against projected values: a pending new or
renamed block can match, and a block whose pending title no longer matches is removed from results.
If search uses an auxiliary text index, that index is an in-memory derivative keyed by the projection
revision, never an independent source of truth. Editing a pending block replaces or composes its
pending intent; it does not create an entity in `authoritative-db`.

`read-snapshot` carries `(appliedServerT, opsRevision, projected-db, op-statuses)`. The pair
`(appliedServerT, opsRevision)` is the cache identity; the projected DB's own basis value is never used
as a server cursor. The OCaml core serializes snapshot/WebSocket application and op-log mutation, then
publishes one new immutable read snapshot. Readers hold one snapshot for the duration of a request so
linked refs, query results, and outline rows cannot observe different overlay revisions.

On any authoritative WebSocket change, the core commits the new base, reconciles explicit operation IDs when
available and otherwise checks submitted semantic intents against the new authoritative value, then
discards the old `projected-db` and every previously compiled projection transaction. It
validates and recompiles the remaining semantic intents in sequence against the new
`authoritative-db`. Stored tx-data, parent/order datoms, or a prior `d/with` result are never replayed.

Each semantic intent stores its operation-specific read set, expected versions, write set, and user
placement rather than precomputed tx-data. Recompilation follows these rules:

| Server change since the intent's base | Projection result |
| --- | --- |
| Does not intersect the intent's read/write set | Recompile the intent against the new DB, except guarded delete whose whole-graph CAS intentionally conflicts |
| Is the authoritative echo carrying the same operation ID | Remove the confirmed op, then compile later dependent ops against the server result |
| Changes an attribute the local intent will overwrite | Mark conflict; never silently choose local or remote |
| Changes a structural neighbor used by insert/move | Re-resolve only if the semantic placement is still unambiguous and its explicit precondition permits it; otherwise conflict |
| Adds/removes/moves/edits anything covered by a delete guard | Conflict; never recompute a broader or different delete set |
| Removes the operation target | Treat delete as satisfied; conflict other operation kinds |

For example, move stores “place A after B” plus source/target-neighborhood preconditions; it does not
store the old `:block/order` transaction. A title edit may survive an unrelated status change, but a
remote title/refs change conflicts. If an op can no longer compile or its precondition fails, it becomes
`conflicted`, is excluded from `projected-db`, and all dependent ops are excluded. Retryable transport
failures remain projected and visibly failed; semantic conflicts restore authoritative data. Guarded
delete persists the stable operation ID as server sync metadata. Non-destructive semantic writes may
be reconciled by their exact intended authoritative value until their endpoints expose operation
identity. Operation IDs are sync metadata, not graph datoms.

#### DataScript history is optional conflict evidence, not the pending model

If the DataScript runtime provides persistent `history`, `as-of`, and `since`, the core can reconstruct
the authoritative DB at an operation's `base_server_t` and calculate the exact authoritative datom
delta before recompilation. That makes read/write-set conflict explanations and three-way comparison
more precise. It does not remove the need for semantic intents, dependencies, operation IDs, transport
state, delete guards, or recompilation on the latest base; DataScript history is not a semantic branch
merge engine.

Because server `t` and DataScript transaction IDs are different domains, each authoritative WebSocket apply
must record their mapping (or store server `t` as transaction metadata). History retention must cover
the oldest live operation's base. Snapshot replacement can truncate older authoritative history, so
operation-specific expected values and delete guards remain the durable fallback even with history.

The checked-in `datascript-ocaml` runtime cannot currently be relied upon for this: its compatibility
`history` returns current facts and `is_history` is false, as documented in
`datascript-ocaml/docs/upstream_differences.md`. Therefore the initial design stores operation-specific
expected identities/attributes and guards in `logseq_pending_ops` and uses WebSocket change identities for conflict
detection. A future real history implementation may replace some duplicated version evidence, but it
must not change the projection or operation lifecycle contracts.

An HTTP 2xx response means only that the server accepted the request. The pending overlay remains and
DataScript is unchanged until the matching authoritative WebSocket event is applied. Reconciliation applies
the WebSocket change to DataScript and removes the matching pending intent as one published model update, so
the UI does not flicker between optimistic and authoritative values. Restart reconstructs the same
projection from the authoritative mirror plus the durable pending log.

#### Wire-level operation identity and confirmation

Every semantic mutation carries `operationId`. Guarded delete stores a compact idempotency record in
the selected graph's existing `sync_meta`: operation ID, target, expected cursor, and accepted cursor.
The same request returns the stored result; reuse with a different target or guard returns 409. This
deliberately avoids changing `tx_log`, event aggregation, or db-sync's general rebase path.

The OCaml change decoder accepts an optional future `operation-ids` field, but the initial delete
contract does not require db-sync to emit it:

```text
id: 48193
event: graph-changes
data: {"format-version":1,"t-before":48192,"t":48193,
       "operation-ids":["mobile-op-uuid"],"upserts":[...],"deleted":[...]}
```

Guarded delete responses return `operationId`, `acceptedT`, and whether the request changed the graph.
The delete remains projected until the local authoritative cursor reaches `acceptedT`. Submitted
title/property writes without an accepted cursor remain projected until a WebSocket rebase shows their
exact intended value. An HTTP response never directly mutates the authoritative local mirror.

The operation log is an app-owned table in the selected graph's `graph.sqlite`, next to but outside
Logseq's DataScript `kvs` table. “Do not write pending state into DataScript” means that pending ops
never become graph datoms or `kvs` values; it does not require a second SQLite file. Keeping the tables
in one file allows graph persistence, cursor update, and op reconciliation to share one SQLite
transaction:

```sql
CREATE TABLE logseq_pending_ops (
  sequence INTEGER PRIMARY KEY AUTOINCREMENT,
  operation_id TEXT NOT NULL UNIQUE,
  graph_id TEXT NOT NULL,
  kind TEXT NOT NULL,
  target_uuid TEXT NOT NULL,
  payload BLOB NOT NULL,
  base_server_t INTEGER NOT NULL,
  state TEXT NOT NULL CHECK (
    state IN ('queued', 'submitted', 'accepted', 'retryable', 'conflicted')
  ),
  attempt_count INTEGER NOT NULL DEFAULT 0,
  created_at INTEGER NOT NULL,
  updated_at INTEGER NOT NULL,
  last_error TEXT
);

CREATE INDEX logseq_pending_ops_graph_queue
  ON logseq_pending_ops (graph_id, state, sequence);

CREATE TABLE logseq_pending_op_dependencies (
  operation_id TEXT NOT NULL REFERENCES logseq_pending_ops(operation_id) ON DELETE CASCADE,
  depends_on TEXT NOT NULL REFERENCES logseq_pending_ops(operation_id) ON DELETE CASCADE,
  PRIMARY KEY (operation_id, depends_on)
);

CREATE TABLE logseq_sync_state (
  graph_id TEXT PRIMARY KEY,
  schema_version INTEGER NOT NULL,
  applied_server_t INTEGER NOT NULL,
  ops_revision INTEGER NOT NULL DEFAULT 0
);
```

`payload` is a versioned Transit value for the closed operation type; it is not arbitrary tx-data.
`sequence` gives deterministic overlay and send order. `base_server_t` records the authoritative
baseline against which the intent was created and is the initial guarded-delete CAS value. `submitted` means
a request is in flight, and `accepted` means HTTP succeeded but WebSocket sync has not confirmed the graph state.
`retryable` remains part of the projection; `conflicted` does not. Explicit dependencies cover edits
to pending-created blocks and structural operations whose source or target is also pending. SQLite
foreign keys are enabled for every graph connection, and `ops_revision` increments in the transaction
that mutates operation rows.

Queued edits to the same still-local block may be coalesced in one SQLite transaction. Submitted or
accepted rows are immutable; a later edit becomes a new ordered row. Enqueue, coalescing/canceling
dependent unsent operations, and delete-overlay creation are transactional. WebSocket reconciliation first
commits authoritative KVS changes and `applied_server_t`, then removes or advances matching operation
rows before publishing one new projection. `retryable` and `conflicted` rows retain visible error
details; explicit discard removes only the op row and reveals the unchanged authoritative mirror.

The initial implementation preserves ADR 001's authoritative KVS/cursor transaction. The app-owned
tables live in the same SQLite file and are copied during snapshot activation, but do not change
db-sync's tx-log or general rebase transaction.

Full snapshot activation stages a fresh authoritative `kvs` mirror. Before atomically replacing the
active file, the importer copies all non-reconciled operation/dependency rows and app-owned sync state
from the active database into the staging database in one transaction. Snapshot replacement must never
silently discard or apply pending intent. DataScript restore ignores these app-owned tables.

For deletion, unsent pending creates inside the confirmed subtree can be canceled atomically with the
delete intent because they have never existed on the server. Any already submitted but unconfirmed
create/update/move in the subtree must settle first; otherwise the delete is not sent and the UI asks
the user to retry after synchronization. This prevents a late self-echo from resurrecting or changing
the subtree after its delete guard was captured.

Split and merge must be exposed only when the server can apply their constituent changes atomically.
Until that contract exists, the renderer, shared title/status editing, insert/move endpoints, collapse,
and delete are still a valid incremental implementation of this ADR.

### 6. Make ordinary block delete shared, explicit, and subtree-aware

Logseq and Outliner modes call exactly the same `delete-block-subtree` command. There is no separate
Logseq delete implementation and no view-specific request shape.

Delete follows Logseq's ordinary block semantics, not its page Recycle semantics. The semantic route
calls Logseq outliner's `delete-blocks` operation. That operation filters the requested roots to
top-level roots, computes each complete subtree, validates protected built-in entities, and removes
the selected roots and descendants from the outline transaction. Page deletion is a different
operation and remains unsupported in Logseq.

Before submission, both modes show a destructive confirmation. The message identifies the block and
the number of descendants that will be deleted. Empty unsynchronized draft blocks may be discarded
locally without a server delete only when they have never been durably created and have no descendants.

The initial guard is deliberately coarse and simple. Confirmation captures the current
`appliedServerT`; the request body contains the stable client operation ID and `expectedServerT`. Inside
the same serialized graph operation that performs `delete-blocks`, the server reads its current graph
`t` and requires exact equality. Any intervening server write, even to unrelated content, returns HTTP
409 and deletes nothing. The user first synchronizes the new authoritative state and then confirms
again. A prior GET followed by an unguarded DELETE is insufficient because it has a
time-of-check/time-of-use race.

This whole-graph compare-and-set is intentionally more conservative than a subtree fingerprint. It
automatically covers descendants added or removed, moves, content/property edits, inbound references,
and other delete side effects without duplicating Logseq's delete-impact algorithm in OCaml and
Clojure. db-sync already exposes the current graph `t` within its serialized semantic handler. A later
version may use real DataScript history or a server-generated delete-impact token to reduce unrelated
conflicts, but it must preserve the same fail-closed behavior.

The existing `DELETE /api/v1/graphs/:graph-id/blocks/:block-id` route is extended to accept this
guarded-delete body. Logseq never invokes it without `expectedServerT`. Other callers may keep
the existing behavior until the server API version removes unguarded deletion.

```json
{
  "operationId": "stable-client-generated-uuid",
  "expectedServerT": 48192
}
```

The serialized handler evaluates requests in this order: authenticate and validate the idempotency
record; if the same operation already completed, return its stored result; compare current
`storage/get-t` with
`expectedServerT`; validate that the target is an ordinary deletable block; then perform one
`delete-blocks` transaction carrying the operation ID. Stale `t` returns 409 with `currentServerT` and
deletes nothing. The guarded response uses the common semantic result envelope rather than an empty
204 so the client receives `operationId`, `acceptedT`, and `changed`.

Conflict behavior is deliberately fail-closed:

| Concurrent or stale state | Result |
| --- | --- |
| Same operation ID was already applied | Return the stored success without applying again |
| Target was already deleted, directly or with an ancestor | A recorded retry succeeds; a new stale request conflicts or returns not found |
| Any server write advanced `t`, related or unrelated | 409; sync and require confirmation again |
| Target moved, subtree membership changed, or affected content/properties/refs changed | Covered by the same `t` mismatch; delete nothing |
| An unsent local-only create exists in the subtree | Cancel it atomically when the confirmed delete is enqueued |
| A submitted but unconfirmed create/update/move exists in the subtree | Do not send delete until it settles; then rebuild the guard and confirm again |
| The same local delete is tapped twice | Reuse the pending operation; never enqueue a second request |
| The same operation ID is retried after an ambiguous transport failure | Return the stored result or 404 success; never apply twice |
| Target is a page, built-in entity, property, class, or malformed cyclic tree | Reject; delete nothing |
| Validation or transaction fails while deleting | Roll back the whole transaction; no partial subtree deletion |

The unrelated-write false conflicts are intentional in a version without undo or block Recycle:
occasionally asking for confirmation again is safer and substantially simpler than an incomplete
client-authored impact fingerprint.

The server also rejects deletion when:

- the target is a page rather than an ordinary block;
- the target is a built-in entity, property definition, or class definition;
- authorization does not permit writes to the graph;
- the subtree cannot be resolved without a cycle or missing required identity.

The UI hides a pending subtree in both modes. Before transport, the core compares its current
`appliedServerT` with `base_server_t` and cancels an already-stale operation. The server CAS remains
mandatory because local preflight cannot close the race. A transport/server failure or 409 removes the
optimistic overlay, restores the authoritative subtree, and reports the conflict. HTTP 404 is treated
as idempotent success because the requested block is already absent. Remote deletes continue to arrive
through WebSocket sync and affect both modes.

This ADR does not add undo, redo, or restore. Because ordinary blocks do not go to Recycle, the
confirmation step must state that the block and its descendants will be deleted. This limitation is
intentional and must not be obscured in the UI.

### 7. Enforce structural invariants in the OCaml core and server

For every insert, move, split, merge, and delete, both the OCaml validator and authoritative server
must enforce:

- stable UUID identity and membership in the selected graph;
- exactly one owning page for every live block;
- a parent that is either the owning page or a live block on that page;
- no self-parenting or ancestor cycle;
- no move into the block's own subtree;
- deterministic sibling ordering with no client-authored raw order key;
- atomic page propagation to descendants when a move crosses page roots, if cross-page moves are
  enabled later;
- no partial application of split, merge, move, or delete;
- guarded delete comparison and subtree deletion occur in one serialized server operation;
- title and encrypted-value handling consistent with the graph's E2EE state.

The initial mobile UI only moves blocks within one page. Cross-page move is rejected even though the
envelope can be extended later. The client sends plaintext protected values only to its local OCaml
core; encrypted graphs use the existing client-side encryption path before transport. Structural UUID,
parent, page, and order identities follow the existing db-sync representation.

### 8. Amend ADR 001's write allowlist narrowly

ADR 001 allowed block creation and property modification but prohibited client-originated delete,
move, and arbitrary transactions. This ADR replaces only that restriction for the closed mobile
operation set above.

The server may accept the typed semantic `insert-block`, `split-block`, `merge-backward`, `move-block`,
and guarded `delete-block-subtree` requests. It must continue to reject raw tx-data, page deletion, arbitrary
property-schema mutation, and any operation kind not in
the versioned allowlist. Existing local-first durability and authoritative WebSocket confirmation rules from
ADR 001 remain unchanged.

## Explicitly out of scope

- undo and redo;
- multi-block selection and batch edits;
- arbitrary drag-and-drop payloads or cross-page moves;
- copy, cut, and paste of block trees;
- block zoom/focus navigation;
- templates and commands;
- page create, rename, delete, or restore;
- property schema creation or class-property editing;
- reactions and comments;
- Recycle browser, restore, garbage collection, or permanent delete;
- arbitrary DataScript transactions;
- desktop-only keyboard command parity;
- persisted or synchronized collapse state;
- rich-text editor parity beyond the existing shared title editor.

These exclusions do not weaken the required operations. In particular, delete is required in both
Logseq and Outliner modes and cannot be deferred behind the absence of undo/redo.

## Rejected alternatives

### Make the first outliner read-only

A read-only tree would satisfy the visual part of the request but not the mobile outliner workflow.
It would also make Logseq and Outliner behavior diverge immediately. The small typed operation set is
preferred.

### Port Logseq's complete outliner and editor namespaces

The Logseq outliner supports batch operations, templates, properties, page lifecycle, reactions,
page recycle administration, plugin transactions, and desktop editor commands. Porting all of it would add
substantial surface area unrelated to the mobile product. This ADR reuses its invariants and preserves
the page-only Recycle boundary while implementing only the required semantic intents in the existing
OCaml core.

### Let Swift and Kotlin calculate order keys and transactions

That would duplicate the most fragile tree rules and allow the platforms to diverge. Platform views
send semantic placement; shared core and server code own validation and order generation.

### Put ordinary blocks in Recycle

Logseq Recycle is a page lifecycle feature. Applying page recycle metadata to ordinary blocks would
invent semantics that Logseq's outliner does not implement. Ordinary blocks therefore use
`delete-blocks`, with subtree confirmation and shared failure handling.

### Optimistically mutate the local graph mirror

The mirror is the exact server state at `appliedServerT`. Writing optimistic structural changes into it
would blur the sync boundary and complicate replay. Pending overlays provide responsive UI without
changing the authoritative mirror.

## Consequences

### Positive

- Journal and page content gain a familiar mobile Logseq outline without a second graph model.
- Logseq and Outliner cannot drift into different mutation or deletion semantics.
- A small operation algebra covers Return, Backspace, indent, outdent, reorder, task changes, and
  deletion without exposing raw transactions.
- Server-owned order generation preserves Logseq ordering behavior across Apple, Android, and other
  clients.
- Whole-graph server-`t` guards, shared confirmation, pending state, failure rollback, and idempotent absence
  handling make destructive actions conflict-safe and retry-safe.
- Excluding undo/redo and desktop-only commands keeps the first implementation bounded.

### Negative

- Guarded delete requires a backward-compatible extension to the authenticated block DELETE route and
  an atomic server-side `t` comparison.
- Split, merge, move, and delete need atomic conflict handling and broader integration tests than the
  current capture/property paths.
- A pending-operation overlay must derive a coherent tree without mutating the local graph mirror.
- Deleted ordinary blocks cannot be restored in Logseq because undo/redo is out of scope and
  Recycle does not apply to them.
- Collapse state does not initially follow the user between devices or launches.

## Rollout and verification

Implementation proceeds in this order:

1. Add pure OCaml tree projection and operation validation tests, including cycles, orphan parents,
   ordering ties, collapsed ancestors, and cross-page rejection.
   Add golden fixtures that run the allowlisted Logseq CLJS outliner op and the OCaml projection
   compiler from equivalent DB states and compare canonical current datoms by UUID/ident.
2. Add server contract tests for each allowed operation and for rejection of every non-allowlisted
   operation.
3. Implement durable optimistic overlays and prove restart/retry behavior before exposing mutations in
   the UI.
4. Add Outliner rendering and the header mode button on Apple and Android.
5. Enable title/status/insert/split/merge/move operations.
6. Enable the shared Logseq/Outliner delete command only after server-`t` CAS, every conflict case above,
   failure rollback, idempotent retry, and WebSocket-echo tests pass.

The ADR is complete when automated tests demonstrate:

- the mode button switches the same journal and regular page between Logseq and Outliner without data
  loss or navigation changes;
- nested blocks render in deterministic Logseq order, and collapse hides exactly the descendants of
  the collapsed block;
- cycles and orphaned parents cannot hang traversal or make blocks disappear;
- one projected DataScript DB makes pending title/ref/tag/property/move/delete effects visible to page
  reads, linked refs, Objects, current-state Datalog queries, search, and Outliner without feature-level
  merge code;
- Apple builds render the mode icon and expose its accessibility label and identifier; Android
  verification is deferred for this implementation pass;
- every supported mobile operation produces the same logical graph on the OCaml client and server;
- golden parity excludes numeric entity/tx IDs but covers page/parent/order, refs/tags/properties,
  implicit supporting entities, subtree effects, and encrypted value representation;
- split and merge are atomic under failure and process termination;
- indent, outdent, move up/down, and insert cannot create cycles or cross-page corruption;
- Logseq and Outliner invoke the same delete operation and confirmation policy;
- deleting a parent removes the expected ordinary-block subtree through Logseq `delete-blocks`;
- any intervening server `t`, duplicate-tap, pending-local-write, and transaction-failure case follows
  the fail-closed table above, including conservative conflicts caused by unrelated server writes;
- offline delete survives restart, remains hidden as pending in both modes, and is submitted once;
- retrying an operation ID is idempotent;
- changed, aggregated, replayed, and idempotent-no-op responses preserve operation identity;
- authoritative WebSocket confirmation clears the pending overlay and advances the cursor, while a no-op
  remains projected until the client reaches and verifies `acceptedT`;
- encrypted graph operations do not expose protected plaintext to the server;
- raw transactions, page deletion, page recycle administration, cross-page move, and every
  unrecognized operation are rejected;
- no undo or redo state, command, RPC action, or UI is introduced.

## References

- `docs/adr/001-real-time-graph-sync.md`
- `../logseq-1/deps/outliner/src/logseq/outliner/op.cljs`
- `../logseq-1/deps/outliner/src/logseq/outliner/op/construct.cljc`
- `../logseq-1/deps/outliner/src/logseq/outliner/core.cljs`
- `../logseq-1/deps/outliner/src/logseq/outliner/recycle.cljs`
- `../logseq-1/src/main/frontend/handler/editor.cljs`
- `../logseq-1/src/main/frontend/modules/outliner/op.cljs`
- `apple/Sources/LogseqModel/Models.swift`
- `apple/Sources/LogseqModel/ViewModel.swift`
- `apple/Sources/Logseq/ContentView.swift`

---

### Agent Guide 001 Lg Lui Rewrite (`docs/agent-guide/001-lg-lui-rewrite.md`)

# LG/LUI Rewrite

## Status

In progress on `codex/lg-lui-rewrite`.

The interaction baseline starts at `main` commit `2bd514d` and must be
refreshed against the then-current `main` before the final cutover. The rewrite
is complete only when it preserves every user-observable interaction from that
baseline on iOS and Android.

## Architecture decision

- Keep the existing OCaml core and its storage, synchronization, E2EE, search,
  FSRS, markup, and outliner behavior.
- Move application state, reducers, effects, projections, and ordinary UI into
  LG and LUI.
- Keep the LUI SwiftUI renderer native on Apple platforms. Render the same LG
  model through LUI's Android backend on Android.
- Keep operating-system services in the existing Swift and Kotlin adapters.
- Implement Logseq-specific editor, rich content, math, code, video, WebView,
  and media behavior as validated LUI extensions rather than standard LUI
  elements.
- Keep Android platform services in the Kotlin Android host and its Kotlin
  adapters.
- Exclude Logseq mode, its composer and send flow, and Logseq-specific automatic
  scrolling from this rewrite. The Outliner keeps its current scroll position
  when it opens and when blocks are added.

## Non-negotiable interaction parity

The rewrite must preserve behavior, not merely expose similarly named
features. The following are authoritative evidence:

1. Every existing `tests/e2e/*.yaml` flow runs unchanged against the rewritten
   application on its declared platform.
2. Existing unit and policy tests remain green or are replaced by equivalent
   LG tests that exercise the same public behavior. Removing a test requires a
   written mapping to stronger coverage.
3. Existing accessibility identifiers remain available with the same meaning.
   Dynamic identifiers retain their current stable UUID/index composition.
4. Navigation depth, back behavior, editor cancellation, focus restoration,
   keyboard dismissal, scroll anchoring, and modal presentation match `main`.
5. Chinese and English IME behavior, UTF-16 caret positions, rapid
   Return/Backspace, editor handoff, autocomplete, drag/drop, long-press
   selection, and haptics match `main`.
6. Offline launch, optimistic edits, pending state, reconnection, WebSocket updates,
   encrypted graphs, assets, capture, sharing, audio, and background refresh
   preserve the current state transitions and feedback.
7. iOS-specific SwiftUI presentation and Android-specific Material behavior
   remain deliberate without coupling either platform to the other's layout.

## Interaction inventory

| Surface | Baseline evidence | Rewrite gate |
| --- | --- | --- |
| Authentication and graph lifecycle | `ios-graphs*.yaml`, `ios-staging-connect.yaml`, `android-staging-connect.yaml` | Same sign-in, graph selection/create/delete, unlock, and offline-open behavior |
| Search and node navigation | `ios-search-*.yaml`, `android-capture-search.yaml`, node/tag/sidebar navigation flows | Same full-screen search, result navigation stack, back, close, query, and status behavior |
| Outliner presentation | `ios-outliner-mode.yaml`, layout, hierarchy, anchor, and page flows | Same ordering, nesting, pagination, collapse, zoom, linked references, and anchor retention |
| Outliner editing | edit baseline, inline editor, continuous editing, rapid-enter, caret, toolbar, autocomplete flows | Same UTF-16/IME, focus, split/merge, pair deletion, toolbar, and handoff behavior |
| Outliner structure | drag, interactions, selection toolbar, empty block deletion flows | Same move, indent/outdent, selection, confirmation, delete, and haptic behavior |
| Rich content and media | `ios-rich-block-rendering.yaml`, YouTube, asset, and audio flows | Same markup actions, code/math/media rendering, playback, and asset presentation |
| Flashcards | `ios-flashcards-regression.yaml` | Same reveal and rating progression |
| Settings and diagnostics | settings tabs/theme/log flows | Same tabs, theme, language, runtime log filtering/copy, and connection settings |
| Sharing and deep links | share image/web/capture/result and shortcut flows | Same extension handoff, staging, result feedback, and destinations |
| Sync and resilience | local realtime, offline, reconnect, restart, snapshot/order regression flows | Same visible state, durability, cursor, and error behavior |

## Delivery gates

1. LG/LUI native runtime shell and reducer tests.
2. Shared LG model with SwiftUI and Android renderer parity tests.
3. Virtualized keyed list, viewport events, anchor preservation, and imperative
   scroll tests on both hosts.
4. Native outliner editor extension and IME/caret stress tests.
5. Rich-content and platform-service extension boundaries.
6. Screen-by-screen migration while preserving the interaction inventory.
7. Full iOS and Android build, unit, integration, and unchanged Maestro suites.
8. Final diff against current `main`, with no unmapped interaction or test.

## Current implementation

The first LG/LUI runtime slice owns selected-graph, sync-status, and search
presentation/query state. Its retained-backend tests prove initial empty state,
in-place status patches, visible sync failures, search input ownership, query
reset, and search-subtree disposal.

---

### Agent Guide 001 Lg Lui Rewrite Android Dogfood Report (`docs/agent-guide/001-lg-lui-rewrite_android-dogfood_report.md`)

# Android Dogfood Report: Logseq

| Field | Value |
|-------|-------|
| Date | 2026-09-01 |
| Target | Android debug app on `logseq_android_36` and physical device `4a23a2e9` |
| Scope | Full Android UI/UX and E2E parity with iOS |

## Summary

| Severity | Count |
|----------|-------|
| Critical | 0 |
| High | 12 |
| Medium | 16 |
| Low | 0 |
| **Total** | **28** |

## Issues

### ISSUE-001: Add sync graph sheet controls overlap

| Field | Value |
|-------|-------|
| Severity | high |
| Category | visual / functional / UX |
| View | Graph picker → Add sync graph |
| Repro Video | N/A; deterministic static layout failure |
| Status | Fixed and verified on emulator |

**Description**

The sheet opens, but the Graph name field, Cancel action, and Add action occupy
the same horizontal area. The action toolbar is not anchored to the bottom, the
form hierarchy is unclear, and the oversized sheet leaves most of the surface
empty. The controls are difficult to read and reliably operate.

Expected: a Material bottom sheet with a readable form, stable keyboard
behavior, and bottom-aligned Cancel/Add actions.

**Repro Steps**

1. Sign in on the Android emulator and wait for the graph picker.
2. Tap **Add sync graph**.
3. Observe the overlapping form and actions.



**Fix Verification**

The Android host now renders one bounded Material sheet child with a vertical
form, flexible spacing, and a bottom-aligned action row. Maestro verified the
field, toggle, disabled/enabled Add state, keyboard entry, and Cancel action.
No LUI, patch-generation, or RenderFlex errors were logged.



### ISSUE-002: Root content and overflow actions render inside system bars

| Field | Value |
|-------|-------|
| Severity | high |
| Category | visual / functional / accessibility |
| View | App frame, first observed on Graph picker |
| Repro Video | N/A; deterministic system-inset failure |
| Status | Fixed and verified on emulator |

**Description**

The Android root scaffold did not consume Android system insets. Titles and
toolbar actions rendered underneath the status bar; the Graph picker overflow
button was visible but its hit target was intercepted by the system bar.

Expected: every screen stays inside the Android status and navigation bar safe
areas, and visible toolbar actions remain tappable.



**Fix Verification**

The Android app frame now owns one root `SafeArea`. A widget regression
test verifies both top and bottom insets, and the overflow button is exposed as
an identified accessibility button. The Settings popup now opens reliably.



### ISSUE-003: Settings opens as a blank modal scrim

| Field | Value |
|-------|-------|
| Severity | high |
| Category | functional / visual / UX |
| View | Graph picker → overflow → Settings |
| Repro Video | N/A; deterministic layout failure |
| Status | Fixed and verified by E2E on emulator |

**Description**

Opening Settings pushed a modal route but rendered only its gray scrim. Android
reported a flex child under unbounded width constraints: the shared iOS-style
language radio picker placed every language in one row, while the Theme and
Language selects themselves had no finite Material form width.



**Fix Verification**

Android now uses a bounded Material select and dropdown for language while iOS
keeps its native radio-group Picker. Both Theme and Language controls have
finite form widths. LG tests pass 125/125, and Maestro verified Settings,
language selection, Tabs, runtime log, back navigation, and dismissal.



### ISSUE-004: Autocomplete can submit a candidate from the previous query

| Field | Value |
|-------|-------|
| Severity | medium |
| Category | functional / UX / race condition |
| View | Journal Outliner → inline editor → tag autocomplete |
| Repro Video | N/A; captured by the repeatable Maestro sequence |
| Status | Fixed and verified on emulator |

**Description**

Typing `#` published the initial candidate list, then typing `E2E Pro` queued
several core-backed text updates. A fast tap could still select row zero from
the old `#` result before the refreshed query arrived. The selected tag was
therefore timing-dependent even though every individual effect succeeded.

Expected: candidates from an older editor value disappear immediately, and a
candidate event cannot overtake pending or in-flight text changes.

**Repro Steps**

1. Open **E2E Journal 08** and edit **E2E Journal Block 08**.
2. Enter `#`, then immediately enter `E2E Pro`.
3. Tap the first candidate before the last text effect resolves.
4. Hide the keyboard and observe that the expected `#E2E Project` chip is
   intermittently missing.

The failed run is retained in
`~/tests/e2e/tests/2026-09-01_094845/android-node-tag-navigation/`.

**Fix Verification**

LG now removes candidates produced for the previous query as soon as editor
text changes and rejects autocomplete selection while that block has a queued
or in-flight text update. Two reducer regressions cover pending and in-flight
ordering. The Android E2E waits for and selects the actual **E2E Project** row.
The node/tag flow and the other nine signed-in Android modules pass in sequence;
LG passes 137 tests and Android passes 68 tests.

### ISSUE-005: Authentication content is centered inside a narrow retained layer

| Field | Value |
|-------|-------|
| Severity | medium |
| Category | visual / responsive layout |
| View | Signed-out authentication |
| Repro Video | N/A; deterministic static layout failure |
| Status | Fixed and verified on emulator |

**Description**

The Android root `Stack` gives non-positioned children loose horizontal
constraints. The authentication column used `grow`, which filled the vertical
Flex axis, but retained its intrinsic 780-pixel width on a 1080-pixel emulator.
The sign-in content therefore appeared left of the physical screen center.



**Fix Verification**

The Android authentication surface now stretches to the available width before
an inner column centers its content. UIAutomator reports
`screen.authentication` at `[0,132][1080,2337]`, and the signed-out Maestro
flow passes against the actionable sign-in control.



### ISSUE-006: Semantic secondary text is transparent throughout Android

| Field | Value |
|-------|-------|
| Severity | high |
| Category | visual / accessibility / content |
| View | Every view using `muted-foreground`; first confirmed on Graph picker |
| Repro Video | N/A; deterministic theme-token failure |
| Status | Fixed in LUI and verified on a physical device |

**Description**

LG correctly emitted the cross-platform `muted-foreground` token, but the LUI
Android backend did not map it into the Material `ColorScheme`. Unknown tokens
fell back to `Colors.transparent`. Supporting text, captions, statuses, and
form guidance remained in the accessibility tree while becoming invisible.

**Fix Verification**

LUI now maps `muted-foreground` to Material 3 `onSurfaceVariant`. The fix and a
backend regression test are pushed as commit
`09631149799f358f55fb6fc13bfa73dfb07e9cf2`; the app pins only its Android
backend to that compatible revision. All 74 tests on that LUI revision pass,
the Logseq Android suite passes 68 tests, and physical-device screenshots
show both page guidance and empty-state descriptions at the expected secondary
contrast.



### ISSUE-007: Empty Graph picker has oversized hierarchy and floating text actions

| Field | Value |
|-------|-------|
| Severity | medium |
| Category | visual / UX / empty state |
| View | Graph picker with no sync graphs |
| Repro Video | N/A; deterministic empty state |
| Status | Fixed and verified on a physical device |

**Description**

The title used the default oversized heading, while **Add sync graph** and
**Refresh graphs** were ungrouped ghost text buttons floating near the top of
an otherwise empty page. There was no empty-state icon, explanation, or clear
primary action.



**Fix Verification**

Android now uses a compact level-three page title and a centered Material empty
state with a cloud icon, secondary explanation, filled Add action, and a
lower-emphasis Refresh action. iOS retains its existing main-branch layout.
A Android-profile LG regression pins the title level, full-width layout,
empty-state presence, action priority, and Material icon names.



### ISSUE-008: Authentication brand name does not match the installed app

| Field | Value |
|-------|-------|
| Severity | medium |
| Category | visual / product identity / regression |
| View | Signed-out authentication and Android launcher |
| Repro Video | N/A; deterministic text mismatch |
| Status | Fixed and verified on emulator and physical device |

**Description**

The Android manifest and launcher identified the package as **Logseq**, but
the shared authentication surface still rendered the old **Logseq** name. This
made the new app difficult to distinguish from an existing Logseq installation.



**Fix Verification**

Both Android and Apple LG profiles now render **Logseq** from the shared
authentication view. A cross-host LG regression rejects the old standalone
name. Android also uses the full product name for clipboard content and the
system share chooser. The signed-out Maestro flow asserts the product name
against the rebuilt native library. The rebuilt debug APK is installed on both
the emulator and physical device.



### ISSUE-009: Adaptive launcher foreground is cramped under OEM masks

| Field | Value |
|-------|-------|
| Severity | medium |
| Category | visual / branding / Android adaptive icon |
| View | MIUI launcher; also applicable to Pixel and themed icons |
| Repro Video | N/A; verified with same-position physical-device screenshots |
| Status | Fixed and verified on a physical device |

**Description**

The sun foreground occupied the full 66-unit adaptive-icon safe diameter. MIUI
applied additional foreground scaling, leaving the outer rays visually cramped
against the rounded-square mask. Other OEM launchers are allowed to apply
different masks and motion, so using the entire nominal safe diameter was too
fragile.



**Fix Verification**

The foreground and monochrome vectors now scale around the 108-unit viewport
center with an additional 14% inset. The monochrome asset uses an opaque black
mask so Android 13+ can apply wallpaper-derived themed colors correctly. A
Android resource regression keeps the canonical Android and Android-host assets
identical and pins their pivot, scale, and monochrome semantics.



### ISSUE-010: Audio action opens a file picker instead of recording

| Field | Value |
|-------|-------|
| Severity | high |
| Category | functional / parity / Android platform integration |
| View | Outliner editor → Audio recording |
| Repro Video | N/A; verified from command routing and missing platform permission |
| Status | Fixed in code; physical-device verification pending OEM install approval |

**Description**

The Android Audio recording command was routed to `ACTION_OPEN_DOCUMENT` and
the Android manifest did not request `RECORD_AUDIO`. Android therefore could
only pick an existing audio file, while iOS recorded, inserted, and played a
new voice note inline.

**Fix Verification**

Android now records AAC audio through `MediaRecorder`, requests microphone
permission at runtime, writes the result into the graph asset metadata path,
hashes it with SHA-256, and inserts it through the authoritative core
`addAsset` action. Audio assets render as a Material inline player backed by
Android `MediaPlayer`, with play, pause, progress, error recovery, and lifecycle
cleanup. Android passes 86 tests, Kotlin unit tests and debug APK compilation
pass. The dedicated physical-device instrumentation APK was blocked before
execution by MIUI's USB installation confirmation (`INSTALL_FAILED_USER_RESTRICTED`),
so the report does not claim a hardware recording pass yet.

### ISSUE-011: Rich media renders as placeholders instead of native content

| Field | Value |
|-------|-------|
| Severity | high |
| Category | functional / parity / rich content |
| View | Journal and page outliner rich blocks |
| Repro Video | N/A; deterministic Android renderer behavior |
| Status | Fixed and verified on emulator |

**Description**

Android rendered video and iframe nodes as a play icon plus their raw URL,
rendered math as italic TeX source, and rendered cloze text as an always-visible
static chip. The nodes had parity identifiers but not iOS-equivalent behavior.

**Fix Verification**

YouTube and iframe content now use a lightweight Material preview and create a
system WebView only after an explicit tap, preserving lazy-list scrolling.
YouTube URLs are normalized to `youtube-nocookie.com`, unsafe schemes are
rejected, and timestamp blocks update the embed start time. LaTeX is typeset
with a pure Android renderer and malformed expressions retain a readable
fallback. Cloze content now reveals and hides through a Material action chip.
Code blocks use selectable, horizontally scrollable syntax-highlighted text
with light and dark Material color palettes; parsed syntax trees are cached so
lazy-list rebuilds do not repeat highlighting work. Unit and widget regressions
cover URL safety, deferred platform-view creation, timestamp parity, math
fallback, cloze interaction, syntax highlighting, and accessibility. The
Android YouTube Maestro flow requires a loaded platform-view identifier rather
than accepting a placeholder.

### ISSUE-012: Android block rows cannot be dragged between positions

| Field | Value |
|-------|-------|
| Severity | medium |
| Category | functional / interaction / outliner |
| View | Journal and page outliner |
| Repro Video | N/A; deterministic missing drag target |
| Status | Fixed in code and widget integration test |

**Description**

A long press emitted the selection/drag-start event, but Android rows were not
draggable and did not accept drops. The core supported before, inside, and
after placement, while the Android surface could never publish one.

**Fix Verification**

Visible lazy-list rows now use `LongPressDraggable` and `DragTarget`. Drop zones
match iOS at the top 25%, middle 50%, and bottom 25%. Drag feedback is a small
Material title card so video, audio, and WebView children are never duplicated.
A widget integration test performs a real long press, moves one block over a
second block, and verifies the LG extension receives the target UUID and
`inside` placement.

### ISSUE-013: Idle Android outliner edits are never committed

| Field | Value |
|-------|-------|
| Severity | high |
| Category | functional / durability / parity |
| View | Every journal and page outliner editor |
| Repro Video | N/A; deterministic host-runtime omission |
| Status | Fixed in code and automated tests; device E2E pending installation |

**Description**

iOS schedules the authoritative core `saveEditing` event one second after text
changes settle. The Android effect drain did not recognize or schedule
that action, so an edit could remain in the transient editor state indefinitely
unless a later structural action happened to commit it.

**Fix Verification**

The Android effect drain now mirrors the iOS debounce policy: text changes and
autocomplete completion schedule one save, newer outliner mutations cancel the
older timer, and visible autocomplete prevents premature saving. The core
executor maps the synthetic effect to `outlinerEvent/saveEditing`. Focused tests
cover request encoding, successful autosave, and autocomplete suppression.

### ISSUE-014: Android Runtime log always appears empty

| Field | Value |
|-------|-------|
| Severity | medium |
| Category | functional / diagnostics / settings |
| View | Settings → Check log |
| Repro Video | N/A; native service returned a literal empty array |
| Status | Fixed and verified on emulator |

**Description**

Android returned `[]` for every runtime-log request. The Settings screen and its
filters were present, but UI/Core diagnostics, ordering, error filtering, and
copying had no real records behind them.

**Fix Verification**

Android now owns a thread-safe 500-record ring buffer with the same source,
level, filter, ordering, timestamp, and JSON semantics as iOS. Host startup,
core restoration, failures, and attachment imports append records without
reading privileged global logcat data. Kotlin tests cover bounds, filtering,
ordering, and JSON escaping. Android Settings E2E now requires visible `INFO`
records for both UI and Core sources. On the rebuilt emulator app, **Check
log** displayed real Android host, Android host, and core-effect records;
level, order, and source controls updated immediately, Copy and Refresh
dispatched without an error, and Done returned to Settings.

### ISSUE-015: Selected Android attachments are discarded

| Field | Value |
|-------|-------|
| Severity | high |
| Category | functional / assets / parity |
| View | Composer and outliner attachment actions |
| Repro Video | N/A; deterministic missing activity-result handling |
| Status | Fixed in code and automated tests; device E2E pending installation |

**Description**

The Android host launched the system document or camera intent but did
not receive its result. Selected files therefore never entered app storage or
the authoritative core asset model.

**Fix Verification**

The host now receives multi-document and full-resolution camera results, copies
them into `Assets`, computes SHA-256 and size metadata off the UI thread, and
dispatches `addAsset` with the originating target block when applicable.
Relative paths resolve consistently with iOS. Structured Logcat and in-app
runtime records cover launch, result, import, and core insertion stages.

### ISSUE-016: Graph catalog only refreshes after a manual action

| Field | Value |
|-------|-------|
| Severity | medium |
| Category | functional / UX / stale state |
| View | Signed-in app → Graphs |
| Repro Video | N/A; verified with lifecycle and effect logs |
| Status | Fixed and verified on emulator |

**Description**

Restoring a signed-in session loaded the saved catalog once, but graph creation
and preparation state could change while Android remained in the foreground.
The visible list stayed stale until the user explicitly tapped **Refresh**.

**Fix Verification**

The Android host now starts a coalescing foreground refresh loop after session
restore, stops it in the background and on sign-out, and immediately refreshes
again on resume. Device logging exposed that the first implementation sent the
selected-graph `refresh` action, so restore and resume both ended in
`automatic refresh.rejected` and never applied the catalog. The automatic path
now sends the catalog-specific `refreshGraphCatalog` action and logs the core
error code/message for any rejected response. On the rebuilt emulator app,
restore completed at 18:54:15 and the next 15-second periodic refresh completed
again at 18:54:29. Five focused tests cover the request contract, diagnostic
rejection, overlap, periodic scheduling, and timer cancellation.

### ISSUE-017: Graphs long list overflows instead of scrolling

| Field | Value |
|-------|-------|
| Severity | high |
| Category | visual / functional / UX |
| View | Graphs with a full remote catalog |
| Repro Video | N/A; deterministic full-state layout failure |
| Status | Fixed and verified on emulator |

**Description**

LUI Android rendered the semantic `list` primitive as an eager `Column`. A
real catalog overflowed the viewport by 2296 pixels, displayed Android's
yellow/black failure stripe, and made the remaining graphs unreachable.



**Fix Verification**

Android now maps `list` to a native lazy `ListView`, matching SwiftUI's native
`List` semantics without changing the Apple backend. The full page scrolls,
starts at the Refresh action, and logs no RenderFlex error. The LUI fix is
pushed as commit `9512238e40248f664d7a870565360436818410ac`; all 77 LUI tests
and all 112 Logseq Android tests pass.



### ISSUE-018: Graphs actions have debug-page hierarchy

| Field | Value |
|-------|-------|
| Severity | medium |
| Category | visual / UX / Android design |
| View | Graphs |
| Repro Video | N/A; deterministic static layout issue |
| Status | Fixed and verified on emulator |

**Description**

**Refresh** is an unadorned full-width text row and **Add sync graph** is only
slightly more identifiable because it has a plus icon. The two primary page
actions consume large vertical bands without a clear priority, while section
headings and graph rows begin below them with no cohesive Material grouping.

Expected: compact Material actions with one unmistakable primary create action,
a lower-emphasis refresh action, consistent icons, and clear catalog sections.



**Fix Verification**

Android now renders one 16dp-inset, 12dp-gap action row. **Add graph** is a
filled primary Material button, **Refresh** is a tonal secondary button, both
are equal width with native icons and 48dp touch targets, and the catalog
continues below as a lazy native list. The implementation is Android-only;
SwiftUI retains the main-branch native List rows. LG passes 144 tests and
Android passes 112 tests. Emulator event logs prove Refresh reaches
`refresh-graphs` and succeeds, while Add graph opens and dismisses the existing
Material form sheet.



### ISSUE-019: Journals loses every bottom action on a populated graph

| Field | Value |
|-------|-------|
| Severity | high |
| Category | functional / layout / UX |
| View | Journals with enough rows to occupy the viewport |
| Repro Video | N/A; deterministic across navigation and app relaunch |
| Status | Fixed and verified on emulator |

**Description**

The journal rows render, but the entire mutually-exclusive bottom chrome is
absent: there is no Capture/Search dock, expanded composer, editor toolbar, or
selection toolbar. Android Back exits directly to the launcher, proving this is
not a hidden editing or selection state. Returning to the app reproduces the
same root screen, leaving no way to capture or search.

Expected: the active bottom chrome remains anchored above the navigation bar
while the journal list independently consumes and scrolls within the remaining
height.



**Root Cause**

The Android-only Capture/Search row incorrectly declared `grow: 1` inside the
bottom stack. LUI correctly translated that value to an `Expanded`, but the
bottom stack is measured with unbounded intrinsic height. Android therefore
could not lay out the flex child, which removed the complete mutually-exclusive
bottom chrome from the rendered surface.

**Fix Verification**

Android now keeps the Capture/Search row at intrinsic height; its Capture child
still grows horizontally, so the primary action fills the available width
without consuming vertical space. Two LG regressions pin that Android-specific
contract, while the separate SwiftUI assertion retains the existing iOS grow
behavior. All 144 LG tests pass. A freshly rebuilt debug APK was installed on
the emulator with the populated graph preserved: Capture/Search remained
anchored above the navigation bar, Capture expanded and dismissed through the
native Back flow, Search opened its Material full-screen presentation, and no
Android, RenderFlex, or overflow exception was logged.





### ISSUE-020: Settings form has unlabeled duplicate selects and weak Material hierarchy

| Field | Value |
|-------|-------|
| Severity | medium |
| Category | visual / UX / accessibility |
| View | Settings → General |
| Repro Video | N/A; deterministic static layout issue |
| Status | Fixed and verified on emulator |

**Description**

The General settings sheet renders two outlined selects that both display
`System`, but neither has a visible field label, so Appearance and Language are
indistinguishable. The form is wrapped in a large white rectangle against an
almost-white sheet, the Tabs summary uses disabled-looking low-contrast blue,
and Cancel/Apply appear as loose text actions at the lower left instead of a
clear Material action area. Large gaps make the settings feel unfinished and
hard to scan.

Expected: a compact Material settings form with persistent Appearance and
Language labels, grouped editor switches and navigation rows, accessible
secondary text, and an anchored Cancel/Apply action hierarchy.



**Root Cause**

LUI's `select` authoring element discarded `:label`, and all three validation
boundaries independently rejected `AccessibilityLabel` on a Select. Android
therefore had no field name to render, while adding the property only in the app
caused the retained LG runtime to reject the complete Settings tree.

**Fix Verification**

LUI now retains the select label in the closed protocol. Android renders it as
a persistent Material `InputDecorator` label; the Apple backend accepts the
same accessibility metadata without changing its visual SwiftUI picker. The
Android form uses tonal cards, readable supporting text, native switches, and
an anchored equal-width secondary/primary action row. LUI passes 201 tests,
Logseq LG passes 144 tests, Android passes 112 tests, and the rebuilt debug
app renders Theme and Language distinctly with no protocol or layout error.



### ISSUE-021: Runtime log sheet overlaps navigation actions and filters

| Field | Value |
|-------|-------|
| Severity | medium |
| Category | visual / functional / UX |
| View | Settings → Check log |
| Repro Video | N/A; deterministic static layout failure |
| Status | Fixed and verified on emulator |

**Description**

The sheet-level **Refresh** and **Done** actions occupy the same vertical band
as the runtime screen's **Errors only** and **Newest first** filters. Labels
and hit targets visibly overlap, so the first filter row is hard to understand
and unreliable to operate. The log body itself is populated, proving this is a
layout composition problem rather than the earlier missing-log-data problem.

Expected: one Material hierarchy with navigation/actions in a non-overlapping
header or footer, compact filter controls below it, and the log list consuming
the remaining scrollable area.



**Root Cause**

The shared sheet exposed the Runtime log screen and its navigation toolbar as
two sibling children. Android bottom sheets compose their single content slot
as an overlay, so the navigation toolbar was painted over the screen's first
filter toolbar. SwiftUI's native sheet backend handles those navigation
children separately and did not exhibit the problem.

**Fix Verification**

Android now receives one bounded vertical sheet child. The Runtime log screen
owns the flexible region, while an equal-width secondary **Refresh** and
primary **Done** row is anchored below it. SwiftUI retains the existing native
navigation toolbar path. The new RED test first proved the old multi-child,
overlay-toolbar contract, then passed after the layout change; all 144 LG and
112 Android tests remain green. On the rebuilt emulator, every filter was
independently tappable, Copy and Refresh dispatched, and Done returned to the
Settings sheet.



### ISSUE-022: Settings Tabs row receives the tap but never navigates

| Field | Value |
|-------|-------|
| Severity | medium |
| Category | functional / navigation / settings |
| View | Settings → General → Tabs |
| Repro Video | [issue-022-tabs-does-not-open.mp4](android-dogfood/videos/issue-022-tabs-does-not-open.mp4) |
| Status | Fixed and verified on emulator |

**Description**

Tapping the full-width **Tabs** row leaves the main Settings sheet unchanged.
The Android event log records the row's Press event every time, so this is not
a missing hit target or Maestro selector failure; the navigation action is
lost after it reaches the LG bridge. The user therefore cannot configure tab
visibility or ordering on Android.

Expected: the Settings sheet immediately replaces its main content with the
Tabs configuration screen, with a clear path back to Settings.

**Repro Steps**

1. Open Settings and keep the General card visible.
2. Tap the full-width **Tabs** row.
3. Observe that the row emits a Press event but the sheet remains on Settings.



**Root Cause**

The Press action correctly set `settings-tabs-open`. Rendering the destination
then created Android icon-only tab-reordering buttons whose authoring maps used
`accessibility-label`; LUI buttons expose that semantic through `label`.
Because the required label was absent from the retained protocol, the backend
rejected the entire destination patch and left the previous Settings tree on
screen.

**Fix Verification**

The reordering controls now publish valid button labels. A new regression test
dispatches Press through the rendered Android row and verifies that Tabs
replaces the main Settings screen, covering the real path rather than directly
sending a model action. All 145 LG tests and 112 Android tests pass. The rebuilt
debug APK entered `screen.settings.tabs` during Android Settings E2E.

### ISSUE-023: Tabs sheet overlaps its back action and misaligns every row

| Field | Value |
|-------|-------|
| Severity | medium |
| Category | visual / layout / Android design |
| View | Settings → Tabs |
| Repro Video | N/A; deterministic static layout failure |
| Status | Fixed and verified on emulator |

**Description**

After navigation succeeds, the Android sheet paints the **Settings** back
action over the large **Visible tabs** heading. The tab rows float around the
horizontal center instead of forming full-width Material settings rows, with
large irregular gaps between labels, selection state, and reorder arrows.
The result is visually broken and also hides the child toggle semantics that
the Settings E2E expects.

Expected: one bounded sheet layout with a non-overlapping navigation/action
area, full-width compact tab rows, stable semantics, and native Android touch
targets.

Evidence: Android Settings E2E artifact
`~/tests/e2e/tests/2026-09-01_192731/android-settings/screenshots/step-037-assertCondition-toggle.settings.tab.jour.png`.

**Fix Verification**

Android now receives one bounded vertical sheet child: a growing Tabs screen
and a separate full-width **Back to Settings** footer. The Android-only screen
uses a compact heading, a native lazy list, 56dp tonal rows, leading tab state,
and trailing reorder controls with stable labels and identifiers. The shared
SwiftUI branch retains its native List and navigation toolbar. The real-Press
LG regression covers layout, navigation, accessibility validation, and leading
alignment; all 145 LG tests pass. The rebuilt debug APK completed the full
Settings E2E, including toggling Flashcards off/on, returning to Settings, and
the Runtime log flow. Final screenshot artifact:
`~/tests/e2e/tests/2026-09-01_195644/android-settings/takeScreenshot/android-settings-material-icons.png`.

### ISSUE-024: Search returns data but renders an empty results surface

| Field | Value |
|-------|-------|
| Severity | medium |
| Category | functional / layout / search |
| View | Journals → Search |
| Repro Video | N/A; covered by the repeatable Capture/Search E2E flow |
| Status | Fixed and verified on emulator |

**Description**

Capturing a task succeeds and the new block appears in Journals, but searching
for its exact title shows a blank surface. The search field contains the full
query and no error is visible, leaving the user unable to open valid results.

Expected: matching pages and blocks appear immediately below the Material
SearchBar and remain navigable through the native Android back stack.

**Root Cause**

Targeted effect logging proved that every query update reached the LG bridge,
the `search-nodes` effect executed successfully, and the final core response
contained three results. The result `list` was nested in a growing vertical
search surface without its own `grow` constraint. Android therefore laid out
the populated list at zero height. SwiftUI's native `List` expands by default,
which is why the shared view had not exposed the same failure on iOS.

**Fix Verification**

The result list now fills the remaining search surface with `grow 1.0`. A new
layout-contract regression first failed against the zero-height list and now
passes. Diagnostic traces report only query length and result count, never
query or result text. The rebuilt debug APK completed the full Capture/Search
E2E: create task, search exact title, open result, verify node, navigate back to
Search, return to Journals, and find the captured task.

### ISSUE-025: Periodic graph refresh blocks editor input and triggers ANR

| Field | Value |
|-------|-------|
| Severity | high |
| Category | performance / functional / concurrency |
| View | Journals composer while automatic graph refresh runs |
| Repro Video | N/A; Android system ANR captured during Composer E2E |
| Status | Fixed and verified on emulator |

**Description**

Rapid composer edits overlap the 15-second automatic graph refresh. Input is
dropped or delayed and Android can show **Logseq isn't responding**.

**Root Cause**

The Android ANR trace showed a background Dart worker inside
`refreshGraphCatalog → snapshot_visible → graph_sidebar_pages → SQLite` while
the foreground input dispatch timed out after five seconds. Core calls run on a
worker isolate, but core and LG share the same OCaml runtime lock. Building a
complete graph snapshot for a catalog-only refresh therefore blocked the UI
isolate when it tried to dispatch composer events into LG.

**Fix Verification**

`refreshGraphCatalog` now returns a dedicated lightweight graph-catalog patch
containing only graph metadata. LG applies only those owned fields and preserves
the active composer, journal rows, navigation, and editor state. Unchanged
catalogs are also skipped by the Android host. Automatic polling runs only while
the authenticated graph picker is foreground; opening a graph stops it so OCaml
network discovery cannot contend with editor input. Core, LG, and Android
regressions cover patch identity, editor-state preservation, polling policy,
unchanged-catalog skipping, and changed-catalog application.
The rebuilt APK then completed the full Composer lifecycle E2E without another
ANR: clear draft, type, collapse, restart, restore exact draft, clear it again,
and return to the collapsed composer.

### ISSUE-026: Node destinations have no visible Android back action

| Field | Value |
|-------|-------|
| Severity | medium |
| Category | functional / navigation / Android design |
| View | Journals → journal or page detail |
| Repro Video | N/A; covered by the repeatable Material Navigation E2E flow |
| Status | Fixed and verified on emulator |

**Description**

Opening a journal or page replaced the root content and title, but the Material
top app bar had no visible back affordance. Android system back still worked,
yet users navigating by touch had no discoverable way to return.

Expected: every non-root destination exposes a 48dp Material back action with
stable automation and TalkBack semantics, while iOS continues using its native
NavigationStack back button.

**Fix Verification**

The Android header now switches its leading action from the sidebar menu to an
`arrow_back` action whenever the LG app-navigation depth is non-zero. The
button uses `button.navigation.back`, has the accessibility label **Back**, and
pops exactly one LG route. The host check keeps the shared SwiftUI renderer
unchanged. The new regression failed before the control existed and now passes;
all 146 LG tests, focused Android extension/icon tests, Android analysis, and
the complete Android Material Navigation E2E pass on the rebuilt APK.

### ISSUE-027: Core restore contends with LG and causes restart ANR

| Field | Value |
|-------|-------|
| Severity | high |
| Category | performance / concurrency / startup |
| View | App restart with a persisted local graph |
| Repro Video | N/A; Android system ANR and repeatable restart flow captured |
| Status | Fixed and verified on emulator |

**Description**

Refreshing the app could render Journals and then stop accepting input until
Android displayed **Logseq isn't responding**. The failure remained even
after catalog polling was disabled for an open graph.

**Root Cause**

The 22:14 Android ANR shows the Dart/UI thread blocked in
`pthread_mutex_lock`, while Dart workers were executing the same
`liblogseq_core.so`. Core and LG share one OCaml runtime lock, but the
Android bridge allowed background Core FFI and foreground LG FFI to enter it
concurrently. Startup `appear`, accessibility, or input dispatch therefore
waited on long graph restore work. The platform log recorded 398 and 169
skipped frames and a 5.083-second focus-event timeout.

**Fix Verification**

A native-runtime scheduler now serializes all Core requests. While Core owns
the runtime, LG UI calls are queued on the Dart side instead of blocking the UI
thread; they flush in order when Core becomes idle, then effect draining
resumes. Three focused scheduler tests cover lock ownership, Core ordering, UI
ordering, and idle notification. Effect/extension tests and Android analysis
pass. The rebuilt APK completed three consecutive forced restarts with sidebar
interaction each time, and Android DropBox contains no ANR newer than the
pre-fix 22:14 record.

### ISSUE-028: Outliner bullet and task status markers do not match iOS

| Field | Value |
|-------|-------|
| Severity | medium |
| Category | visual / interaction hierarchy / cross-platform parity |
| View | Journals and node outliner rows |
| Repro Screenshot | `ui-comparison/android/journals.png` |
| Status | Fixed and verified on emulator |

**Description**

Android rendered the ordinary block bullet from a full Material circle glyph,
making it much larger and heavier than iOS. Task rows then added a second
generic 16dp status icon beside it. The two independent controls looked
crowded, and status colors from the shared `task-*` palette were unsupported by
the Android backend and could resolve to transparent. The status context menu
also required a long press, and its standard `accent` foreground token rendered
its labels and icons transparent.

Expected: preserve iOS main's separate interaction semantics—bullet zooms into
the block and task status changes status—while matching its 7pt bullet, 22pt
status glyph, and 24pt alignment slots.

**Fix Verification**

Android now paints a dedicated centered 7dp bullet behind the existing zoom
target and a centered 22dp task glyph behind the existing status target. Task
states use the same row foreground as iOS, and retained rows reactively swap
the icon without introducing Android-only status colors. SwiftUI continues
through its original icon/button path. LUI Android now opens menu-only buttons on primary
tap and maps the shared `accent` and `destructive` tokens to Material primary
and error roles. The 147-test LG suite covers marker geometry and retained-row
updates; LUI's 79 Android tests cover menu activation and semantic colors. A
rebuilt debug APK was installed on the emulator, where primary tap opened the
visible native menu and selecting Done immediately produced the full-ring check
glyph and completed text treatment. A follow-up icon-source comparison found
that Android still used materially different silhouettes for backlog, doing,
review, and done (ellipsis, calendar, rectangular comment, and broken-ring
check). Those now use the closest native Material equivalents to iOS's dashed
circle/progress ring/comment-check/full-ring check semantics. The complete
121-test JVM suite and `gradlew test` pass, and the rebuilt debug APK
shows the Done marker as a centered full-ring check without changing SwiftUI.





### ISSUE-029: Selection toolbar overflows and selected blocks have no state

| Field | Value |
|-------|-------|
| Severity | high |
| Area | Outliner / Selection / Android UI |
| Status | Fixed and verified on emulator |

**Description**

The shared selection toolbar supplied iOS's icon-plus-caption buttons with a
46pt fixed height. Android correctly rejected the unsupported top icon
placement, but the remaining Material button needed a 48dp minimum target and
rendered red vertical overflow warnings across every action. Plain Android
outliner rows also used a `box` to preserve editor gestures, so they could not
project the selected state that iOS shows immediately after a long press.

**Fix Verification**

Android now receives seven icon-only Material actions with stable accessibility
labels, 48dp targets, compact spacing, and horizontal scrolling retained for
narrow screens. SwiftUI keeps its existing captioned Liquid Glass toolbar.
LUI Android now supports `selected` on a generic box, exposes the state to
semantics, and paints the standard Material secondary-container selection
surface; the shared outliner projects its selected signal onto the existing
gesture-compatible row box. The dedicated visual Maestro flow confirms all
seven actions are visible with no overflow and the selected row is visibly
highlighted. LUI's 80 tests, the LG 148-test suite, the app's 121 Android tests,
and `gradlew test` all pass.

### ISSUE-030: Android modal roots lose their accessibility identifiers

| Field | Value |
|-------|-------|
| Severity | high |
| Area | Dialogs / Sheets / Android E2E |
| Status | Fixed and verified on emulator |

**Description**

The Graphs screen loaded and refreshed correctly, but its Android E2E could
not address the visible delete confirmation dialog by
`dialog.graph-delete`. The same omission affected every Android dialog and
bottom-sheet root, making Settings and form workflows vulnerable to false
inaction reports and inaccessible automation.

**Root Cause**

LUI's modal controller built the real `DialogRoute` and
`ModalBottomSheetRoute` surface directly. That path bypassed the normal node
wrapper that applies `Semantics(identifier: ...)`, so only modal children kept
their identifiers. The invisible retained controller still had the identifier,
but the foreground surface exposed none to Android accessibility.

**Fix Verification**

LUI now applies the declared accessibility identifier to the actual dialog or
sheet surface. New widget tests cover both modal kinds and failed before the
fix. All 80 LUI Android tests, 121 application Android tests, and both analyze
gates pass. On the rebuilt APK, the Graphs E2E now long-presses the local graph
row using the native Android context-menu gesture, addresses the visible
delete dialog and its actions, cancels it, opens the Add graph sheet, verifies
its form controls, and returns to the catalog successfully.

### ISSUE-031: Rapid editor input sends redundant updates and autocomplete taps are lost

| Field | Value |
|-------|-------|
| Severity | high |
| Area | Outliner editor / Autocomplete / Android responsiveness |
| Status | Fixed and verified on emulator |

**Description**

Typing an autocomplete query quickly dispatched every intermediate value into
LG. Tapping a visible candidate while one of those updates was pending closed
the candidate toolbar but did not insert the selected tag.

**Root Cause**

The Android editor synchronously dispatched every intermediate text value into
LG, so a single keyboard burst repeatedly crossed the Dart/OCaml boundary on
the UI thread. LG then discarded `ChooseOutlinerAutocomplete` whenever a
text-change effect was pending or in flight. This avoided stale selection
state by throwing away the user's action instead of preserving its order.

**Fix Verification**

Android now coalesces an ordinary editor text burst for 80ms and publishes only
the latest title and caret, while newline and disposal cancel the pending
timer. LG queues the candidate-choice effect behind pending and in-flight text
changes instead of dropping it. The focused Android regression failed with
three immediate events before the change and now observes one latest event;
the LG regression failed because the choice was absent and now verifies exact
effect ordering. All 122 Android tests, Android analysis, and all 148 LG tests
pass. On the rebuilt APK, the complete node/tag E2E types `#E2E Pro`, selects
`E2E Project`, verifies the inserted tag chip and tagged-node navigation, and
finishes without an ANR.

The separately observed cold-start ANR was not caused by editor input. Android
reported `Application does not have a focused window` while the debug Android
process was still initializing and compiling JIT code on the emulator. Startup
phase logs now distinguish binding, first frame, platform-state load, bridge
load, LUI initialization, and root mounting so a future launch failure can be
classified from evidence instead of being attributed to the active editor.

### ISSUE-032: Android text sharing fails when the share title contains spaces

| Field | Value |
|-------|-------|
| Severity | medium |
| Area | Android E2E / Sharesheet entry |
| Status | Fixed and verified on emulator |

**Description**

The application handled a valid text share, but the E2E runner could not launch
the intent when its title was `Android share`. The local shell preserved the
quoted argument, then `adb shell` parsed the remote command a second time and
treated `share` as a separate package argument.

**Fix Verification**

The runner now escapes the title for the remote shell and has a focused runner
regression that checks the exact adb argument. The complete Android text-share
flow opens Capture, preserves the shared title and URL, and passes on the
emulator.

### ISSUE-033: Shared images are absent from the lazy journal and render as files

| Field | Value |
|-------|-------|
| Severity | high |
| Area | Android sharing / Journal projection / Rich media |
| Status | Fixed and verified on emulator |

**Description**

Android successfully imported a shared image and the core returned its asset
UUID, but the current journal did not update. When forced into the projection,
the same asset rendered as a paperclip rather than an image.

**Root Cause**

`addAsset` returned a full snapshot. LG intentionally preserves the active
lazy-list window across unrelated full snapshots, so the new row was omitted.
The core also normalizes `image/png` to `png`, while Android only recognized
MIME values beginning with `image/`.

**Fix Verification**

Journal asset insertion now returns a bounded structural outliner splice built
from the live visible context. The Android renderer recognizes both MIME types
and normalized image extensions. Core and Android regressions failed before
the fixes; all core tests, all 122 Android tests, analysis, and the Android
image-share E2E now pass with the preview visible in the current journal.

---

### Agent Guide 002 Ocaml Platform Boundary (`docs/agent-guide/002-ocaml-platform-boundary.md`)

# OCaml Platform Boundary Implementation Plan

Goal: Give OCaml sole ownership of shared application and sync decisions while Swift and Kotlin execute native services and render LUI projections.

Architecture: The existing OCaml application model, session, and pending-operation interpreter remain authoritative.
Platform adapters execute explicit byte-oriented requests, socket commands, timer commands, and native presentation requests.
The migration preserves current storage and wire compatibility and removes obsolete platform projections after their remaining callers are replaced.

Tech Stack: OCaml 5.5, Dune, LUI, Swift 6, SwiftUI/UIKit, Kotlin, Jetpack Compose, URLSession and OkHttp.

Related: Builds on `001-lg-lui-rewrite.md`, `../adr/001-real-time-graph-sync.md`, and `../adr/002-mobile-outliner-mode.md`.

## Problem statement

Swift currently combines native services with WebSocket protocol handling, graph bootstrap decisions, retry policy, editor save policy, and a second application snapshot reducer.
Android contains corresponding protocol and scheduling implementations.
The same app behavior can therefore diverge by platform even though the application model and graph operations already live in OCaml.

Stages 1–3 ran in `/Users/tiensonqin/Codes/projects/logseq-chat-ocaml-boundary` on branch `logseq/ocaml-platform-boundary`.
The user then requested merging into the latest main and continuing directly on main.
Integration uses `/Users/tiensonqin/Codes/projects/logseq-chat`, whose existing capture/share/rendering work was committed as `3e0b780` before the merge.
Keep that work and run all integration regressions before continuing on main.
Commit each major change separately; do not push unless requested.

## Testing Plan

Use the existing OCaml RPC/session integration harness to exercise the real graph session and resulting transport commands with deterministic time and response events.
Cover initial connection, coalesced remote notifications, multiple pull pages, local pending operations, schema reset, disconnect, reconnect, graph switches, stale responses, offline transitions, cancellation, and snapshot deferral during editing.
Exercise HTTP failure and duplicate-checksum recovery through the real pending pump and verify request identity remains stable after retries.
Exercise editor events through the shared application and session, including IME/transient text, autocomplete, Return and backspace, route changes, save cancellation, and stale timer delivery.
Verify native consumers execute the same shared commands rather than interpreting graph values.
Retain native input reconciliation, caret and layout smoke checks.

Run focused tests before the full `rtk proxy dune build @shared/native/runtest` gate.
Never run `swift test`.
Use Swift build and existing standalone native harnesses for Apple checks.
Run Android compilation and focused Gradle tests when the installed SDK/toolchain supports them.
Serialize all Dune invocations and scripts that invoke Dune.

NOTE: I will write *all* tests before I add any implementation behavior.

## Ownership contract

| Concern | Owner | Platform responsibility |
| --- | --- | --- |
| Graph, editing, navigation and sync state | OCaml | Deliver typed user and OS events |
| WebSocket protocol, cursor and reconnect policy | OCaml | Open/send/receive/close sockets and wait requested delays |
| Pending HTTP decisions and recovery | OCaml | Execute one requested HTTP/upload operation |
| Save timing and autocomplete policy | OCaml | Schedule/cancel a requested timer and return its token |
| Snapshot and outline projection | OCaml | Render patches and retain native view identity |
| IME, local text reconciliation, caret and geometry | Native UI/LUI backend | Own transient interaction state |
| Camera, photo, audio, credential storage and share extensions | Native host | Read/write OS resources and return results |

## Stage 1: Baseline and legacy cleanup

1. Record the base revision and production/test symbol references before deletion.
2. Establish core and native build baselines in the isolated worktree.
3. Remove unused section/preorder projection methods from `apple/Sources/LogseqModel/ViewModel.swift` after proving they have no production callers.
4. Remove unreachable legacy policy groups from `apple/Sources/Logseq/OutlinerEditing.swift` and update tests that only exercise removed APIs.
5. Retain policy helpers reachable through production helpers, native editor reconciliation, focus, text measurement, and active rich-content rendering.
6. Run compilation and the existing behavioral checks.

## Stage 2: Shared WebSocket and graph lifecycle decisions

1. Add integration cases in `shared/test/logseq/rpc_test.ml` for socket events and explicit outgoing commands.
2. Confirm failures occur because the shared session does not yet produce the required commands.
3. Introduce a small typed shared sync driver under `shared/src/logseq/core/`, with a matching `.mli`.
4. Integrate it with `session_types`, `rpc_session`, and the existing `sync_session` cursor and event application boundaries.
5. Publish commands for connect, send, close, wake, snapshot refresh, and pending-sync wake through the existing RPC snapshot boundary.
6. Replace JSON frame interpretation and pull/cursor bookkeeping in Swift `ViewModel.runGraphEventsOnce` and Kotlin `AndroidGraphEventsSync` with command execution and raw event delivery.
7. Move reconnect, snapshot deferral, and graph-generation decisions from `LGGraphLifecycle` and `AndroidGraphLifecycle` into the shared driver.
8. Keep token acquisition, background-task lifetime, URLSession/OkHttp, file streaming, and native cancellation in platform code.
9. Verify graph switches reject stale socket/timer results and no FFI call holds the core worker while waiting for network traffic.

## Stage 3: Shared pending transport recovery

1. Add real pending-pump tests for transport loss, retry exhaustion, duplicate checksum, paginated asset lookup, conflict, and cancellation.
2. Verify the missing shared behavior fails before implementation.
3. Move retry budgets and error interpretation into the OCaml pending transport state. Audit confirmed the current raw PUT upload uses stable asset paths and metadata headers; the old paginated checksum resolver is specific to the unused semantic POST upload. Remove that obsolete native resolver and verify raw-upload conflicts remain failures rather than inventing a successful upload.
4. Preserve operation UUID, transaction identity and expected cursor across retries.
5. Reduce Swift `PendingSyncTransport.swift` and Kotlin `AndroidPendingSync.kt` to one-request executors.
6. Keep path resolution, native streaming, headers, OS transport errors and cancellation in platform adapters.
7. Run pump integration tests and platform transport checks.

## Stage 4: Shared editor save policy

1. Add real application/RPC cases for debounced saves, autocomplete suppression, atomic structure events, route changes and stale timers.
2. Verify the cases fail because scheduling still belongs to the platforms.
3. Make the shared application/session emit generic deadline effects with cancellation/generation tokens.
4. Let Swift and Kotlin execute timer effects without inspecting text, autocomplete or graph state.
5. Remove duplicate save policy from `LGRuntime.swift`, `ViewModel.swift`, `GraphSyncHTTP.swift` and `NativeEffectDrain.kt` as the new path replaces it.
6. Keep native text input and frame-level event coalescing only where it preserves IME/caret behavior, with ordering specified by the shared event contract.
7. Validate Return/backspace, fast typing, completion selection, background save and graph-switch behavior.

## Stage 5: One application projection

1. Trace remaining `LogseqStore.snapshot` consumers and distinguish native presentation data from domain decisions.
2. Add integration checks for partial responses, row splices, deletion, selection, route retention and background sync responses.
3. Expose the needed projection through the existing OCaml/LUI response path and replace domain consumers of Swift merged snapshots.
4. Remove the Swift `mergedSnapshot` domain reducer and redundant state/model helpers after all live callers are migrated.
5. Keep typed platform DTOs and native render caches where they are needed to execute commands without rebuilding UI identity.
6. Verify native rendering does not require a second graph/outline model and confirm Android uses the same projection contract.

## Stage 6: Integration and delivery

1. Run the full OCaml gate and platform compilation checks.
2. Run native editor, navigation, search, share and background-sync regression checks relevant to the touched boundaries.
3. Record measured code removal, remaining platform responsibilities, verification output and any environment limitations in this plan.
4. Reconcile the finished changes into the original workspace, preserving all unrelated and prior feature edits.
5. Re-run checks affected by integration and leave the result reviewable.
6. Commit each major stage separately after validation, as requested by the user.
7. Mark the goal complete only after all required stages have been implemented and verified.

## Progress

| Stage | Status | Evidence |
| --- | --- | --- |
| Plan and isolation | Complete | Worktree based on `ab3ce02`; prior workspace preserved |
| Legacy cleanup | Complete | Removed 26 unreachable type groups and unused section/preorder projection; 604 production Swift lines and 1046 obsolete test lines removed; 776 core tests and LogseqModel build passed |
| Shared sync driver | Complete | 782 core tests, 36 Android JVM tests, Swift model build, real WebSocket command harness and iOS simulator App/extension build passed; snapshot recovery and reconnect commands execute on both platforms |
| Pending recovery | Complete | 789 core tests, 37 Android JVM tests, iOS App/extension build, Swift model build and native retry-delay harness passed; stable request/transaction identity, cleanup, graph switches and Unicode error summaries covered |
| Shared autosave | Complete | 799 core tests, 39 Android tests and native autosave command checks passed; shared idle-save/cancellation/sync effects replace both platform timers. A direct Swift Testing simulator host runs the 126-test UI suite; async fixtures, background cancellation and preservation of operation errors during render were fixed. Legacy Store projection cleanup follows in Stage 5 |
| Single projection | Complete | Core-owned snapshot projection removes the Swift patch/splice reducer and old Store typing/autosave pipeline. Native delete confirmations enter the shared effect queue; external pending work wakes one shared sync effect. 804 core, 78 standalone Swift model, 127 Swift UI and 40 Android JVM tests passed; Android native core and iOS App/Share/Widgets/Shortcuts builds passed |
| Integration and verification | Complete | Integrated on main; 806 core, 78 Swift model, 127 Swift UI and 42 Android JVM tests passed. Final iOS simulator App/Share/Widgets/Shortcuts and Android arm64 native core builds passed; simulator launch, shared-provider import, search, editor and actual HTTP/WebSocket regressions passed |

## Testing Details

Protocol tests send real events into the production shared session and assert actual outgoing requests and observable graph state.
Pump tests execute actual operation transitions and replay rather than testing copied platform algorithms.
Autosave integration tests assert actual shared deadlines, committed graph data and cancellation behavior.
Native harnesses exercise production editor and presentation implementations without XCTest.
Deletion-only cleanup is verified with reference auditing and compilation rather than new tests that mirror source structure.

## Implementation Details

- Keep signatures and implementations synchronized.
- Prefer extending existing RPC effects and session ownership over adding a second orchestration runtime.
- Keep all network and timer waits outside the OCaml runtime lock.
- Scope asynchronous events with graph and connection generation identifiers.
- Preserve existing wire and persisted operation formats unless a backward-compatible optional field is sufficient.
- Use vectors or bounded queues for growing operation/event sequences and avoid quadratic list append patterns.
- Preserve platform token handling and credential-store boundaries.
- Recheck source references immediately before deleting code.
- Report checkpoints while continuing within the user-authorized goal.
- Do not include unrelated work in commits or overwrite concurrent changes.

## Question

No user decision is required to start.
Platform-specific APIs and packaging remain native, while shared application behavior moves to OCaml.
Any unsupported build environment will be recorded with the exact failed command and independent validation completed where possible.

---

## Consolidation Notes

The native Store requests `projection: "snapshot"` for maintenance RPC calls. The core replaces successful partial projections with its canonical visible snapshot; errors and ordinary full responses are preserved. Interactive LUI effects keep the existing bounded patches, including 32-content viewport hydration on 10,000-row pages. Swift holds decoded data for platform services and does not merge graph rows, splices or selections.

The retired Store-only editor API had no production caller after native deletion confirmation was moved to the shared host/effect contract. Its timer, coalescing and native patch tests were removed with the API; active editor behavior remains covered by shared reducer/session tests, the core/native autosave integration suite and native IME tests. `LogseqContentMode`, `LogseqBlockSection`, mutation-cursor bookkeeping and the Swift snapshot-refresh predicate were also unused or replaced. Snapshot import now checks editing inside the serialized core, including edits begun during download.

Final integration fixes cover forced Android snapshot recovery, rejection of late downloads after graph changes, and preventing pending-sync results from scheduling their own pump. The `isPendingSyncProjection` response marker survives complete native Store projections, so a permanent HTTP failure cannot restart its exhausted retry budget through UI updates. Android recovery downloads and imports fresh data, preserves editor state until the core validates import, and publishes the recovered snapshot.

Search presentation now uses the system searchable activation and keyboard focus. The redundant SwiftUI focus binding, task and scroll handler were removed; native scroll dismissal remains. The simulator search fixture waits for the host view's actual appearance before measuring interaction. The unchanged first-open/reopen limits and typed-text/dismissal checks passed at 508/179/114 ms.

## Final Verification

- Shared native core: 806 tests, including actual session/bridge autosave, shared sync, stable upload retries, snapshot import safety and passive pump projections.
- Swift model suite: 78 tests through the direct Swift Testing runner; native UI suite: 127 tests in the simulator app host. Both runners bypass the unsupported XCTest bridge.
- Android JVM suite: 42 tests with no failures or skips.
- Native simulator regressions: shared autosave commands, serial effect delivery, Return handoff, 12 caret layout comparisons, search input/dismissal/reopening and text/photo/screenshot/PDF/Markdown provider import.
- Actual OCaml session plus Swift HTTP transport: interrupted upload, lost response, permanent 403, bounded 503 retries and cancellation; original 1 MiB asset bytes and identity preserved.
- Snapshot HTTP handling: 12 schema/cursor/row-count/compression/error cases; real WebSocket command execution forwards opaque frames and generation IDs.
- iOS simulator core/App/Share/Widgets build and signed-out app launch passed, including four App Shortcuts and native HTTP/upload/crypto/runtime export checks. Final Android arm64 OCaml/JNI/SQLite library build also passed.

Compared with integrated main (`3e0b780`), production Swift sources remove 1,339 net lines (205 added, 1,544 deleted). Native Swift remains responsible for SwiftUI/UIKit rendering, retained view identity, IME/caret and editor geometry, OS presentation, camera/photos/audio, Share/App Intents/Widgets, secure credentials and native HTTP/WebSocket/timer execution. OCaml owns application projection, editor semantics/autosave, synchronization protocol, recovery commands, retry budgets and error summaries.

---

### Agent Guide 003 Shared Domain Consolidation (`docs/agent-guide/003-shared-domain-consolidation.md`)

# Shared Domain Consolidation Implementation Plan

Goal: Remove obsolete Swift application APIs and give OCaml ownership of the remaining shared effect, bootstrap, capture, and editing semantics.

Architecture: OCaml emits complete native service requests and consumes their results through the existing serialized core boundary.
Swift and Kotlin retain native rendering, transient input reconciliation, credentials, HTTP streaming, filesystem access, and operating-system integration.
Do the work directly on main in `/Users/tiensonqin/Codes/projects/logseq-chat` and commit each completed stage separately.

Tech Stack: OCaml 5.5, Dune, LUI, SwiftUI/UIKit, Swift Testing, Kotlin/Jetpack Compose, URLSession and OkHttp.

Related: Builds on `002-ocaml-platform-boundary.md`.

## Problem statement

The first boundary migration removed the platform snapshot reducer, shared sync protocol, retry policy, and autosave decisions.
Obsolete Store wrappers remain, and native hosts still translate core effects into core RPC calls, interpret snapshot bootstrap metadata, assemble shared capture text, and implement a few editor/media semantics.
These rules should have one implementation and behavioral coverage in OCaml.

## Testing Plan

Start with the existing shared native gate and direct Swift model runner.
For each migration, first add all relevant behavior tests and observe the expected failures before implementation.
Use production application/session paths to verify graph mutations, effect completion, failure propagation, capture idempotence, cursor/schema selection, graph switching, and stale download rejection.
Cover transcript insertion with asset creation, malformed metadata, failed import, offline cached graphs, locked encrypted graphs, capture title deduplication, Unicode pair deletion, and media timestamp association.
Keep native transport, IME, caret, Return handoff, share-provider, search, and extension packaging checks.
For deletion-only changes, audit production references and retain meaningful behavior coverage at the actual shared entry points rather than adding tests for absent symbols.

Run these commands from the repository root, serializing all commands that invoke Dune:

```sh
rtk proxy opam exec --switch=5.5.0 -- dune build @shared/native/runtest
rtk proxy python3 scripts/test-swift-model-suite.py
rtk proxy env LOGSEQ_OPAM_SWITCH=5.5.0 scripts/build-mobile-ios-simulator.sh
rtk proxy python3 scripts/test-swift-ui-suite.py
```

Inspect the Android build instructions before running the existing JVM test gate and native arm64 build.
Run the existing native HTTP/WebSocket, snapshot, shared-provider, and editor smoke scripts affected by the final implementation.
Never run `swift test` or XCTest.

NOTE: I will write *all* tests before I add any implementation behavior.

## Stage 1: Remove obsolete Store APIs

1. Audit references in `apple/Sources`, `apple/App`, scripts, and test fixtures before each deletion.
2. Remove production-unreachable Store convenience methods and their private payload/state helpers from `apple/Sources/LogseqModel/ViewModel.swift`.
3. Remove tests that only exercise retired wrappers while retaining or moving meaningful core, queue, and transport coverage to production entry points.
4. Remove unused `OutlinerYouTubeTimestampPolicy.targetURLsByBlockID` from `apple/Sources/Logseq/OutlinerEditing.swift` and its retired-only tests.
5. Remove unreachable branches and passthrough policy helpers where reference analysis proves they add no behavior.
6. Run core, Swift model, and affected UI compilation checks and commit.

## Stage 2: Execute shared core effects in OCaml

1. Add integration cases in `shared/test/logseq` that execute real capture/task/search/navigation/outliner/flashcard effects and inspect real session responses.
2. Include invalid payloads, unknown effects, stable identifiers, transcript child creation, failure propagation, and repeated delivery.
3. Move the semantic mapping currently in `apple/Sources/Logseq/LGEffects.swift` and `android/app/src/main/kotlin/com/logseq/app/runtime/CoreEffectExecutor.kt` to a shared OCaml executor.
4. Use the existing native bridge/RPC boundary for execution rather than adding another platform orchestration layer.
5. Preserve effect identifiers, native result delivery, platform command revisions, draft cleanup, and user-visible error behavior.
6. Keep time and randomness supplied through existing core facilities or explicit host inputs, with deterministic test injection.
7. Make asset and transcript creation one core-owned operation and remove the native second mutation.
8. Run shared and both native executor regressions and commit.

## Stage 3: Consolidate snapshot protocol and bootstrap decisions

1. Add session-level cases for valid/invalid cursor metadata, schema fallback, relative download URLs, cached graph activation, encrypted graphs, force recovery, and late results after graph switches.
2. Move request construction, metadata interpretation, and sequencing from `apple/Sources/LogseqModel/GraphSyncHTTP.swift` and the Android snapshot downloader into shared core commands.
3. Move lifecycle decisions from `apple/Sources/Logseq/LGGraphLifecycle.swift`, `LogseqStore.bootstrapSelectedGraph`, and corresponding Android lifecycle code to the shared session.
4. Preserve progress reporting, native temporary-file ownership, streaming, decompression, token acquisition, cancellation, and background task lifetime.
5. Keep filesystem probing native but report facts to the core instead of interpreting graph state in the host.
6. Run core bootstrap and real native snapshot/first-sync/encrypted-graph regressions and commit.

## Stage 4: Consolidate shared capture semantics

1. Add core behavior cases for text/title/URL combinations, duplicate titles, empty input, Unicode, and deterministic repeated capture IDs.
2. Move canonical note text assembly and deep-link semantics from `apple/Sources/Logseq/SharedCapture.swift` into the core.
3. Keep App Group queue persistence, provider loading, App Intents, file staging, checksums, and OS URL delivery native.
4. Preserve compatibility with already queued shared captures and deliver their stable identifiers unchanged.
5. Run text/photo/screenshot/PDF/Markdown provider and shortcuts regressions and commit.

## Stage 5: Consolidate editor and media semantics

1. Add core editing cases for empty node-reference deletion at valid/invalid UTF-16 ranges, selected text, Unicode, and ordinary backspace.
2. Add markup projection cases for YouTube URLs, timestamp association, explicit targets, and safe embedded-media URLs.
3. Move those rules from `apple/Sources/Logseq/OutlinerEditing.swift` into shared editor/markup projections and make native renderers consume the result.
4. Keep native text reconciliation, IME marked text, caret geometry, Return handoff buffering, view identity, and WebKit presentation.
5. Run shared editor/markup tests and native caret/Return/media regressions and commit.

## Stage 6: Full verification and final audit

1. Run the full shared native, Swift model, Swift UI, and Android JVM gates on the final revision.
2. Build iOS App/Share/Widgets/Shortcuts and Android arm64 native core.
3. Run affected HTTP/WebSocket, snapshot, capture/provider, search, and editor integration smoke checks.
4. Review the diff for compatibility, duplicated domain rules, accidental public API retention, and quadratic sequence handling.
5. Record exact results, remaining native responsibilities, and any concrete environment limitation here.
6. Commit the final verification documentation and leave main clean.

## Progress

| Stage | Status | Evidence |
| --- | --- | --- |
| Plan | Complete | Current main is the implementation workspace |
| Obsolete APIs | Complete | Removed unused send/search/navigation/flashcard/status wrappers and payloads; preserved serialization, main-actor responsiveness, durable response and transport tests through production capture and graph entry points; 56 model tests pass |
| Core effects | Complete | Shared executeEffect dispatcher replaces Swift/Kotlin mappings; transcript persistence/projection and binary-upload activation covered; 817 core, 109 Swift UI, 42 Android JVM tests pass |
| Bootstrap | Complete | Core owns bootstrap/cache/lock/sync eligibility and snapshot protocol; generations reject stale same-graph downloads; 824 core, 63 model, 109 UI and 41 Android tests plus 12 actual HTTP cases pass |
| Shared capture | Complete | Shared canonicalization, idempotent IDs and deep-link routing; raw metadata queue reads legacy entries; late Android ASR results become shared effects; 828 core, 105 UI, 40 Android tests and real text/photo/screenshot/PDF/Markdown provider checks pass |
| Editor/media | Complete | Core projects validated media URLs, embed requests and timestamp targets; core declares synchronous input rules consumed by native IME/caret adapters; 832 core, 63 model, 102 UI and 41 Android JVM tests pass |
| Final verification | Complete | 832 core, 56 model, 102 UI, 41 Android tests; both native builds, transport/provider/bootstrap/editor/search integrations pass |

## Testing Details

Tests assert persisted graph state, real outgoing service requests, canonical projected markup, and observable native interaction.
Tests must not merely assert copied JSON assembly or mock call counts.
Retired convenience-method tests can be removed only after their meaningful behavior is covered through the live core/runtime entry points.

## Implementation Details

Android speech recognition remains asynchronous.
A transcript already available at capture time is staged atomically with its asset.
A later recognition result is a separate native service event; the core creates its child once without delaying the original audio capture.


- Keep `.ml` and `.mli` changes together.
- Serialize all Dune operations, including mobile wrapper builds.
- Keep network and timer waits outside the core executor.
- Scope asynchronous work to graph/generation identifiers.
- Preserve persisted queue, graph, checkpoint, and wire compatibility.
- Keep Swift/Kotlin OS adapters narrow and explicit.
- Commit each major stage after its relevant checks pass.
- Record deviations with evidence in this document.
- Do not install or publish a new build as part of this source consolidation unless requested.

## Boundary audit

The remaining Swift is responsible for native rendering and OS integration: UIKit/SwiftUI view identity, keyboard/IME state, caret geometry, Return handoff buffering, WebKit/AVPlayer presentation, authentication/keychain access, native HTTP/WebSocket streaming, file staging, background task lifetime, App Group queue storage, share providers, widgets and App Intents.
These responsibilities still require native adapters and should stay in Swift.

The core now owns application effect translation, graph bootstrap and sync eligibility, stale download generations, snapshot protocol metadata, canonical shared text and deep links, asset/transcript mutations, media URL normalization and timestamp association.
Editor syntax rules are declared by the core and applied synchronously by a generic native UTF-16 adapter; waiting for an asynchronous core response inside UIKit or Compose backspace handling would break IME and caret timing.
Legacy queue entries, pending asset intents without transcripts, and additive markup fields remain decodable.
The legacy YouTube target wire property remains for compatibility, with no native domain interpretation.
The unused Swift built-in task-status list/choice merger and identity-only pending-pump helper have also been removed.

The final deletion pass removed seven wrapper/passthrough-only tests; meaningful scheduler, nonblocking transport and durable UI publication cases now exercise production capture/graph adapters.
Actual shared-core tests cover search semantics and journal pagination.
The native integration fixture opens a fresh graph before exercising mutations, matching production startup; opening only the catalog is not sufficient.


## Verification record

Verified on 2026-10-21, directly on main.

| Check | Result |
| --- | --- |
| Shared native gate, `dune build @shared/native/runtest` | 832 tests pass; final gate rechecked successfully |
| `scripts/test-swift-model-suite.py` | 56 tests in 5 suites pass |
| `scripts/test-swift-ui-suite.py` | 102 tests in 11 suites pass |
| Android `:app:testDebugUnitTest -PlogseqRequireNativeCore=false` | 41 tests pass, no failures/errors/skips |
| iOS simulator build | Main app, Share Extension, Widgets and four Shortcuts packaged and verified |
| Android native arm64 build | `liblogseq_core.so` built successfully |
| `scripts/test-ios-shared-domain.py` | Real graph creation/import, capture/task/transcript persistence across reopen, and actual LUI-issued UTF-16 input policy pass |
| `scripts/test-ios-snapshot-download.py` | 12 real HTTP cases pass, including malformed cursor and layered/corrupt gzip |
| `scripts/test-ios-sync-commands.py` | Actual socket sends core bytes and returns opaque frames with generation |
| `scripts/test-ios-sync-recovery.py` | Six close/failure/cancellation modes pass |
| `scripts/test-ios-first-sync-graph.py` | Cold launch uses the configured self-hosted server |
| `scripts/test-ios-encrypted-graph-setup.py` | Native account/graph keys and WebCrypto interoperability pass |
| `scripts/test-ios-share-providers.py` | Actual text/photo/screenshot/PDF/Markdown provider, queue and import checks pass |
| `scripts/test-ios-install-regressions.py` | Both checks pass |
| `scripts/test-ios-device-scripts.sh` | Passes with isolated local configuration and current build expectations |
| `scripts/test-ios-native-interactions.py` | 12 caret comparisons, rapid Return/Shift+Enter, delayed caret acknowledgements, core autosave and three search presentation/dismissal cycles pass |

The native Return fixture originally used a nested synchronous RunLoop, which prevented its queued model acknowledgement from running inside the async app test host.
It now awaits the event loop; rapid Enter/Shift+Enter typing and delayed caret acknowledgements pass without changing product input behavior.
Device-script checks also referenced a removed production-build restriction and sourced developer signing defaults; those tests now inspect current build settings and isolate configuration.

The final native search check exposed an existing iOS 26 search-field relocation: UIKit borrowed the already focused search field into the toolbar, causing two focus acquisitions and one focus loss during each opening.
Sampling showed `UISearchController` borrowing the field and triggering keyboard relayout.
The iOS 26 renderer now embeds one `UISearchTextField` directly and keeps pending native typing until matching core acknowledgements arrive.
New stable-focus and delayed Unicode typing assertions fail against the prior renderer and pass against the new one.
Visual inspection also caught an empty-content sizing issue; the search surface now fills its viewport, and a geometric check verifies the input stays at the bottom above the keyboard.
The final simulator screenshot was visually checked at `/tmp/logseq-consolidation-search.png`.
The fixture separately exercises a plain UIKit text field to account for system keyboard initialization before applying the unchanged 0.8-second first-search and 0.45-second reopening limits; it does not preload the keyboard in production.
The final verified search run took 185/62/61 ms, with one focus acquisition and no intermediate focus loss in each cycle.

Compared with the starting revision `972f784`, production Swift changed by +325/-1508 lines (net -1183).
No XCTest invocation, physical-device installation, TestFlight submission, or Android emulator run is part of this consolidation.

---

---

### Agent Guide 004 Android Ios Release Qa Report (`docs/agent-guide/004-android-ios-release-qa_report.md`)

# Android UI/UX、iOS 对照与 Release 差距实测报告

> 后续修复与复测见 [005-android-release-quality_report.md](005-android-release-quality_report.md)。本文保留初测版本的结果；正文、公式与代码展示的旧缺口已在后续修复中处理。

日期：2026-10-21（Asia/Shanghai）  
版本：2.0.2；源码 revision：`5db32d1833ab8229c220ebe4e6c005e32cddeceb`

## 结论

**当前不建议按正式 Release 的完成状态验收。优先解决两件事：Android 富文本会漏显示正文；Release 包的日记滚动仍有明显长帧。** 此外，Android 尚未实现与 iOS 相同的公式排版和代码语法高亮。

这轮实际运行了 Android App、录制原生视频、测量帧统计并采集 Perfetto trace；没有把历史报告或源码中的所有潜在问题重新列一遍。红米没有连接，本报告不能确认那台手机的具体卡顿原因，也不能给出 Android 比 iOS 慢多少倍的结论。

## 测试条件与证据

| 项目 | 本轮使用的条件 |
| --- | --- |
| Android | `logseq_android_36`，API 36.1，arm64，1080×2400，60 Hz，4 vCPU、2 GB guest RAM |
| GPU | 显式 `-gpu host`；SurfaceFlinger 确认 Apple M3 OpenGL ES Translator。最初的软件渲染环境不用于 Release 结论 |
| 构建 | 当前源码重新构建 Profile 与 Release；Release 无 `DEBUGGABLE`，仅用本地 Android debug key 签名以便模拟器安装，并非正式发布签名 |
| 数据 | 内容对照：8 篇日记的现有 E2E fixture；性能复测：100 篇日记 × 8 blocks，含文本、引用、公式、代码，不含 WebView |
| iOS | iPhone 17 Pro / iOS 26 模拟器；同一内容 fixture，使用仓库离线 UI test host，链接生产渲染器和 OCaml core |
| iOS 边界 | test host 绕开登录，平台 presenter 使用测试 executor。因此可比较实际内容渲染，不能据此验收真实登录、系统分享和同步，也不能把 host 的 Settings 断言失败判成正式 App bug |
| 负载限制 | 宿主机存在内存压力及其他进程负载。探索阶段还与编译重叠；正式 Release 滚动复测在构建结束、重启 Android 后执行。所得绝对耗时仍是本模拟器上的观测值 |

[原生 Android demo](android-ios-release-qa/2026-10-21/android-demo.mp4)：`adb shell screenrecord` 原始 MP4，1080×2400，101.07 秒，无音轨，展示快速捕获、侧栏、闪卡与滚动。录制的是 Profile 包，没有加速或剪掉失败操作。

[证据说明与复测脚本](android-ios-release-qa/2026-10-21/README.md)包含原始日志、截图、帧数据与 trace。两端内容截图：[Android](android-ios-release-qa/2026-10-21/android-rich-content.png)、[iOS](android-ios-release-qa/2026-10-21/ios-rich-content.png)。

## 已确认的 UI/UX 差异及功能缺口

| 项目 | Android 实际表现 | iOS 对照 | 判断 |
| --- | --- | --- | --- |
| 引用正文 | `E2E Rich Quote` 只剩竖线，文字未显示 | 同一 fixture 完整显示引用文字 | **P1：正文显示缺失，阻止发布验收** |
| 粗体正文 | 性能 fixture 的 `with **bold text** and ...` 显示成 `with and ...`，`bold text` 消失；Release 中同样出现 | 共享协议中正文存在；iOS 渲染器处理嵌套内容 | **P1：同一渲染协议错误**。此处不是数据库数据丢失的证据 |
| 数学公式 | 显示等宽文本 `E = mc^2`、`x_0 + y_2 = z_2` | 实际排版指数与数学字体 | **尚不支持真正的 TeX 数学排版** |
| 代码块 | 等宽纯色文本；未显示 `swift` 语言标签 | 显示语言标签与语法高亮 | **尚不支持完整语法高亮/语言展示** |
| 界面风格 | Material 侧栏、较平的胶囊控件、Material 底部 sheet | 原生浮动工具栏、材质与不同的排版密度 | 合理的平台差异，不要求逐像素照搬 iOS |

正文缺失的原因已经核对到数据契约：共享层在 [markup.ml](../../shared/src/logseq/core/markup.ml#L68) 中将引用和 emphasis 正文编码为 `children`；Android 的 [appendNode](../../android/app/src/main/kotlin/com/logseq/app/ui/extensions/OutlinerBlockContent.kt#L319) 与引用分支却读取 `node.optString("text")`。应递归渲染节点子内容并保留嵌套样式，不能只补一段 fallback 文案。

公式和代码展示是当前实现明确保留的简化：[AndroidMath / AndroidCodeBlock](../../android/app/src/main/kotlin/com/logseq/app/ui/extensions/OutlinerBlockContent.kt#L635)。这些是本轮有截图和源码双重证据的缺口；没有把未测试的功能都称为“不支持”。

实际完成的操作包括快速捕获提交并在日记中看到新 block、侧栏切换到闪卡、展示 cloze 答案，以及 Release 中打开 Settings。录屏中设置齿轮曾连续点击无响应，但重启并在 Release 包复测后已能打开：[复测截图](android-ios-release-qa/2026-10-21/android-release-settings.png)。因此不将它列为已确认的功能故障。ASCII 输入也已操作；没有足够证据声称中文 IME 或连续输入延迟通过验收。

## 性能：实际测到了什么

### 1. 不含 WebView 的 Release 日记滚动仍卡顿

相同的 100×8 fixture，每轮向下滑动 4 次、向上滑动 4 次，滑动持续 400 ms，每次间隔约 150 ms。每轮前重置 App 的 `dumpsys gfxinfo`，采样期间没有录屏。

| Release 滚动轮次 | 渲染帧 | Janky frames | 掉帧比例 | 帧耗时 P50 / P95 / P99 |
| --- | ---: | ---: | ---: | --- |
| 1 | 130 | 61 | 46.92% | 34 / 150 / 200 ms |
| 2 | 108 | 67 | 62.04% | 48 / 200 / 250 ms |
| 3 | 211 | 59 | 27.96% | 26 / 150 / 200 ms |

三轮合计 449 帧中 187 帧被 Android 标记为 janky，约 **41.6%**。这是系统帧统计的比例，不是平均 FPS；百分位来自系统直方图，存在分桶精度。采样前后截图确认实际滚动了日记，画面没有被系统对话框遮挡。

原始数据：[Release JSON](android-ios-release-qa/2026-10-21/release-100x8/android-performance.json)、[采样前](android-ios-release-qa/2026-10-21/release-100x8/screen-before.png)、[采样后](android-ios-release-qa/2026-10-21/release-100x8/screen-after.png)。

### 2. 长帧集中在主线程布局、重组，并伴随 JIT

另外采集的 12 秒 Release trace 中观察到：

| 片段 | 单次最长耗时 |
| --- | ---: |
| `AndroidOwner:measureAndLayout` | 264.1 ms |
| `Compose:recompose` | 184.2 ms |
| ART 编译 `AndroidComposeView.screenToLocal...` | 657.7 ms |

该 trace 中 App 主线程累计 CPU 时间约 3,919 ms，RenderThread 约 1,019 ms，JIT 线程约 636 ms，`DefaultDispatch` 约 63 ms。**就这次滚动场景而言，应先检查 Android UI 的布局/重组和首次执行成本；现有证据不支持直接把瓶颈归给 OCaml core。** 各 trace 片段可嵌套，不能相加当作一次操作耗时；CPU 时间也不等同于用户等待时间。

含视频的早期 Profile trace 还测到 WebView 绘制最长约 82 ms、lazy prefetch 最长约 136 ms。但 Release 的无 WebView 场景也存在长帧，所以只优化视频嵌入不足以解决问题。

证据：[Release trace](android-ios-release-qa/2026-10-21/release-100x8/scroll.perfetto-trace)、[片段统计](android-ios-release-qa/2026-10-21/release-100x8/trace-summary.csv)、[线程 CPU](android-ios-release-qa/2026-10-21/release-100x8/cpu.csv)。

下一步应在 trace 中继续定位昂贵布局对应的具体行与重复重组范围，检查 lazy item 的稳定 identity/content type、滚动状态读取范围和主线程计算；这是基于本次 trace 的调查方向，不是已验证的修复方案。参照 [Compose 性能实践](https://developer.android.com/develop/ui/compose/performance/bestpractices)。对首次触控的 JIT 停顿，应实测关键交互的 [Baseline Profiles](https://developer.android.com/topic/performance/baselineprofiles/overview) 效果，不能用它掩盖持续的布局成本。

### 3. 没有证据支持的性能结论

- Release 的静置 10 秒产生 **0 个 App 新帧**，没有复现空闲页面持续重绘。此时 gfxinfo 的 `4950 ms` 百分位是无帧样本占位值，不是 4.95 秒卡顿。
- 三轮滚动后的 App PSS 约 126–130 MiB，没有单调增长；样本不足以判断内存泄漏，也没有证明 App 内存占用是红米卡顿根因。
- 冷启动样本波动很大，且存在系统刚启动的负载。本轮不将它压成一个“正常冷启动耗时”或两端速度排名。`first_ui_rendered` 在 `doOnPreDraw` 上报，`journals_ui_ready` 在行布局回调上报，都不等于用户已看到并可操作的完整页面。
- 曾出现 **Process system** ANR，随后手势被系统弹窗拦截，产生“滚动但 App 渲染 0 帧”的无效样本。这些样本已单独保存并排除；不能把系统 ANR 写成 Logseq ANR，也不能把 0 帧解释为流畅。

因此，报告支持“当前模拟器上的 Release 滚动确实有长帧”，尚不能确认红米上具体的 CPU、GPU、内存、IME 或同步占用比例。

## 距离正式 Release 还差什么

按本轮证据，建议按以下顺序收敛，而不是先增加更多功能：

| 优先级 | 要完成的事 | 可签字的验收结果 |
| --- | --- | --- |
| P1 | 修复引用、粗体等嵌套富文本的正文显示 | 同一 fixture 两端正文完整；覆盖嵌套样式与引用，不以控件存在代替内容正确 |
| P1 | 降低日记滚动的布局、重组和 JIT 长帧 | 用同一 100×8 graph 在稳定模拟器及用户红米上重复 Release 测量，记录帧分布与 trace；先建立明确预算。可将持续滚动 jank <5%、P95 <32 ms 作为讨论起点，属于建议目标，并非已达成指标 |
| P2 | 补公式排版与代码展示 | 公式、上下标、代码语言与高亮的视觉结果达到明确的产品验收标准 |
| 发布前证据 | 完成真实 App 的关键闭环 | 在真实登录状态下验证编辑落盘/重启保留、双端同步和附件往返，并补中文 IME；本轮离线渲染对照不能替代这些验收 |
| 发布工程 | 让自动化和发布包可复验 | 修正 fixture 活动目录及 Maestro 截图路径问题；帧采样拒绝弹窗/未滚动/0 帧的无效样本；验证正式签名包。当前 Gradle Release 产物为 unsigned APK，本轮签名只用于本机测试 |

本轮已有两类测试工具问题影响证据可靠性：Android 将图谱迁移到 `UI_Regression-uiregression` 后，往旧的 `uiregression` 目录写 fixture 不会改变活动图谱；Maestro 2.8 拒绝指向本轮输出目录之外的绝对截图路径。相关失败已保留，不能计为产品回归，也不能计为测试通过。

本次未改产品代码，未运行 `swift test`。Android Profile 与 Release 均构建成功；iOS 渲染库 build-only 检查成功。原 iOS App 的 Documents 与偏好设置已恢复，内容对照使用单独的 test host。

---

### Agent Guide 005 Android Release Quality (`docs/agent-guide/005-android-release-quality.md`)

# Android Release Quality Implementation Plan

Goal: 修复已实测的 Android 正文显示缺失、补齐公式与代码展示，并用可重复的 Release 测量降低滚动成本。

Hard constraints (user, 2026-10-21): performance work must preserve iOS layout and styling, and must not add unnecessary complexity. Keep changes local to Android; reject mechanisms without demonstrated benefit.

Architecture: 保持共享 OCaml 模型及现有 Kotlin/Compose 宿主。
首先修正 Android 对共享 markup 树的消费，再将昂贵的展示计算移出重复的布局路径。
性能改动以实际 trace 和行为回归为依据，保留编辑、导航、虚拟化与拖放语义。

Tech Stack: Kotlin、Jetpack Compose、Android instrumentation/JUnit、OCaml/Dune、Maestro、Perfetto。

Related: Builds on [004-android-ios-release-qa_report.md](004-android-ios-release-qa_report.md).

## Problem statement

工作目录为 `/Users/tiensonqin/Codes/projects/logseq-chat`。
计划起点源码 revision 为 `5db32d18`。
保留已有报告及其他任务的工作，不修改无关文件。

共享协议将 emphasis 和 quote 的正文放入 `children`，Android 却读取 `text`，因此正文漏显。
数学块目前只是等宽文本，代码块没有语言标记与高亮。
报告中的无 WebView Release 滚动样本有 27.96%–62.04% janky frames，trace 中布局、重组与 JIT 存在长片段。
这些模拟器数据需要在同一环境重新建立基线，不能直接解释红米真机情况。

## Testing Plan

每个阶段均先写齐该阶段的行为回归与边界测试，确认因现有行为而失败，再开始该阶段实现。
不以检查测试标签、类型或文件包含某个字符串替代真正的渲染与交互验证。

| 阶段 | 验证行为 | 测试与证据 |
| --- | --- | --- |
| 1 正文 | 嵌套粗体/斜体/删除线、引用中的链接与页面引用完整显示，链接仍可点击，完成状态保留样式 | 新增 Android rich-content instrumentation tests，经真实 LUI wire/store/extension 渲染，不 mock renderer |
| 2 展示 | 公式上下标、分式与非法表达式；代码语言标签、真实彩色 spans、未知语言、空内容和深色主题 | 先增加仪器测试与必要的纯渲染工具单测，再接入离线公式与高亮实现；截图复核 |
| 3 性能 | 大图谱滚动后的正文、标题、编辑焦点、返回位置正确；虚拟化仍有界，旧行状态不串到新行 | 复用并扩展 AndroidOutlinerParityTest；针对被 trace 确认的失效行为补回归 |
| 4 测量 | 正确定位活动图谱，拒绝 ANR/错误前台/未滚动样本；同配置的前后帧分布可比较 | Python 测量工具测试、Release 三轮滚动、Perfetto 与原生截图/录屏 |

NOTE: I will write *all* tests before I add any implementation behavior.

## Task checklist

- [x] Read the planning and TDD guidelines, report, root instructions and current Android renderer.
- [x] Create this implementation plan and record scope, edge cases, verification and open questions.
- [x] Phase 1: write all failing rich-content tests and verify RED.
- [x] Phase 1: correct recursive markup rendering, verify GREEN, simplify and recheck.
- [x] Phase 2: write all failing math/code rendering tests and verify RED.
- [x] Phase 2: implement offline formula layout and themed code highlighting, verify GREEN and screenshots.
- [x] Phase 3: establish a repeatable Release baseline and identify the concrete hot path.
- [x] Phase 3: preserve the earlier failing interaction/accessibility regressions and establish nine passing backend contracts before the behavior-preserving pipeline refactor.
- [x] Phase 3: implement the smallest durable optimization, verify GREEN and interaction parity.
- [x] Phase 4: write failing measurement-validity tests, fix fixture/state handling and verify GREEN.
- [x] Run the relevant Android unit/instrumentation gates and Release build.
- [x] Repeat the same Release performance scenarios, inspect traces and capture after screenshots/demo.
- [x] Update the report with actual improvements, remaining limits and test results; complete the goal only when its implementation and verification are done.

## Phase 1: preserve the complete markup tree

Files:

- `/Users/tiensonqin/Codes/projects/logseq-chat/android/app/src/androidTest/kotlin/com/logseq/app/AndroidRichContentTest.kt` (new).
- `/Users/tiensonqin/Codes/projects/logseq-chat/android/app/src/main/kotlin/com/logseq/app/ui/extensions/OutlinerBlockContent.kt`.
- `/Users/tiensonqin/Codes/projects/logseq-chat/android/app/src/main/kotlin/com/logseq/app/ui/extensions/AndroidRichText.kt` (only if extraction improves reuse).

1. Build a small test harness using the existing `logseqExtensionRegistry`, `LuiBackend.applyJson` and Compose test rule.
2. Feed actual shared markup shapes through `create-extension` and assert visible text and link callbacks for nested emphasis, quotes, Unicode, completion decoration, empty children and malformed markup fallback.
3. Run the complete new test class and retain expected failing assertions before touching production code.
4. Recursively consume children under their inherited style and annotations; keep quote text interactive and size its decoration to its actual content.
5. Cache parsed/grouped markup by encoded input and avoid rebuilding identical annotated content on unrelated recompositions.
6. Re-run the new class and existing outliner parity tests; use the existing fixture for a screenshot comparison.

The expected rendering contract includes this concrete input:

```json
{"type":"emphasis","style":"bold","children":[{"type":"text","text":"bold text"}]}
```

Its visible text must remain `bold text`.
The implementation must not require a duplicated `text` field from the shared layer.

## Phase 2: render math and code without adding WebViews

Files:

- `/Users/tiensonqin/Codes/projects/logseq-chat/android/app/build.gradle.kts`.
- `/Users/tiensonqin/Codes/projects/logseq-chat/android/app/src/main/kotlin/com/logseq/app/ui/extensions/OutlinerBlockContent.kt`.
- `/Users/tiensonqin/Codes/projects/logseq-chat/android/app/src/main/kotlin/com/logseq/app/ui/extensions/AndroidMath.kt` (new if needed).
- `/Users/tiensonqin/Codes/projects/logseq-chat/android/app/src/main/kotlin/com/logseq/app/ui/extensions/AndroidCodeBlock.kt` (new if needed).
- `/Users/tiensonqin/Codes/projects/logseq-chat/android/app/src/androidTest/kotlin/com/logseq/app/AndroidRichContentTest.kt`.

1. Add the complete math/code test set first and confirm the old plain-text implementation fails the intended assertions.
2. Validate a local Android Canvas TeX implementation and a pure Kotlin syntax engine using their primary documentation and real dependency resolution.
3. Keep renderer/library details behind small Android helpers; prefer existing Maven Central artifacts and record the selected versions.
4. Perform nontrivial parsing/highlighting away from repeated main-thread composition; cache by source, language, theme and text size as applicable, with bounded lifetime.
5. Preserve accessible source text, clear invalid-expression fallback, horizontal access to wide formulas/code and theme contrast.
6. Keep unknown languages readable, preserve all code characters and whitespace, and show a language label when supplied.
7. Test offline in light/dark appearance and remeasure rich-row scrolling after adding the renderers.

Candidate primary references:

- [JLatexMath Android](https://github.com/noties/jlatexmath-android) for offline Canvas formula rendering.
- [Highlights](https://github.com/SnipMeDev/Highlights) for Kotlin syntax highlighting including Swift.

These are implementation candidates, not a requirement to use a particular library if dependency or rendering validation fails.

## Phase 3: optimize the measured scrolling work

Initial inspection paths:

- `/Users/tiensonqin/Codes/projects/logseq-chat/android/app/src/main/kotlin/com/logseq/app/ui/extensions/OutlinerBlockContent.kt`.
- `/Users/tiensonqin/Codes/projects/logseq-chat/android/app/src/main/kotlin/com/logseq/app/runtime/LogseqRuntime.kt`.
- `/Users/tiensonqin/Codes/projects/logseq-chat/shared/src/logseq/view_outliner.ml`.
- `/Users/tiensonqin/Codes/projects/logseq-chat/android/app/src/androidTest/kotlin/com/logseq/app/AndroidOutlinerParityTest.kt`.
- `/Users/tiensonqin/Codes/projects/logseq-chat/duniverse/lui/platform/android/lui/src/main/kotlin/dev/lui/LuiVirtualList.kt` and `LuiNodeView.kt` (dependency investigation).

1. Retain the original report samples and capture a fresh pre-change Release baseline in a separate evidence directory.
2. Use 100 journals × 8 blocks, the same gesture sequence, refresh rate and GPU mode; build completely before measurement.
3. Separate first-use/JIT measurements from warmed scrolling; validate foreground, screenshot content, actual movement and absence of system dialogs.
4. Identify whether work is repeated span construction, virtual-window invalidation, unnecessary wrapper layout, composition reuse or code compilation before selecting the change.
5. Write the selected behavior regression before implementing; preserve the ten-thousand-row scroll/edit/back test.
6. Prefer removing repeated work and unnecessary layouts over lowering visual fidelity, disabling virtualization, or changing data semantics.
7. If the real defect belongs to LUI, make the change durable in its source repository and update the app's pinned dependency rather than relying on an ignored `duniverse` edit.
8. Evaluate app Baseline Profiles only for measured JIT costs; do not call AOT compilation a fix for repeated expensive layouts.

Native row integration is an option only if the trace shows that the existing generic row hierarchy remains the dominant cost after simpler fixes.
It must preserve status/priority, tags, collapse, edit, selection, page navigation, assets and drag/drop, and requires shared-protocol/iOS fingerprint updates if the extension schema changes.

## Phase 4: make acceptance evidence repeatable

Files:

- `/Users/tiensonqin/Codes/projects/logseq-chat/scripts/test-android-e2e.sh`.
- `/Users/tiensonqin/Codes/projects/logseq-chat/scripts/test-android-e2e-runner.sh`.
- `/Users/tiensonqin/Codes/projects/logseq-chat/tests/e2e/android-rich-block-rendering.yaml`.
- `/Users/tiensonqin/Codes/projects/logseq-chat/docs/agent-guide/android-ios-release-qa/2026-10-21/measure-android.py`.
- `/Users/tiensonqin/Codes/projects/logseq-chat/docs/agent-guide/004-android-ios-release-qa_report.md`.
- `/Users/tiensonqin/Codes/projects/logseq-chat/docs/agent-guide/005-android-release-quality_report.md` (new result report).

Add measurement-tool behavior tests before changing state/fixture handling.
Resolve the canonical graph directory from persisted metadata or the platform storage policy; do not assume the graph ID is the active directory name.
Use Maestro-compatible relative screenshot names/output directories and retain failure artifacts.
Reject obstructed, nonmoving or zero-frame scrolling samples and keep unsupported measurements distinct from regressions.
Record device, revision, variant, compilation mode, dataset, warmup, host conditions, sample counts, frame distribution, memory and trace paths.
Report comparisons for the same scenario; do not claim the proposed <5% jank / P95 <32 ms target has passed without measurements.

## Verification commands

Run from `/Users/tiensonqin/Codes/projects/logseq-chat`.
Keep Gradle workers/heap bounded while a simulator is running; do not measure performance during compilation.

```sh
rtk proxy ./android/gradlew -p android --max-workers=2 --no-parallel -Dorg.gradle.jvmargs=-Xmx2g -PlogseqRequireNativeCore=false :app:testDebugUnitTest
rtk proxy ./android/gradlew -p android --max-workers=2 --no-parallel -Dorg.gradle.jvmargs=-Xmx2g -PlogseqRequireNativeCore=false :app:connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.logseq.app.AndroidRichContentTest
rtk proxy ./android/gradlew -p android --max-workers=2 --no-parallel -Dorg.gradle.jvmargs=-Xmx2g -PlogseqRequireNativeCore=false :app:connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.logseq.app.AndroidOutlinerParityTest
rtk proxy ./android/gradlew -p android --max-workers=2 --no-parallel -Dorg.gradle.jvmargs=-Xmx2g :app:assembleRelease
```

Instrumented tests must use a current matching native core; do not use `logseqRequireNativeCore=false` when shared OCaml or protocol inputs have changed since the native build.
Expected final result is passing relevant suites and a successful Release build, with separate pre/post measurement artifacts.

If shared code changes, run the required core gate serially:

```sh
rtk proxy opam exec --switch=5.5.0 -- dune build @shared/native/runtest
```

Apply @ocaml-development for core work and @test-driven-development for implementation.
If editing LUI OCaml views, first read its required reactive skill.
If Swift changes become necessary, use build-only validation with the native-link environment; never run `swift test`.

## Testing Details

Visible text, preserved annotations, actual link callbacks, formula pixels/layout, highlighted spans and scroll/edit/back behavior are the assertions.
Tests must fail for the missing behavior before implementation, and existing interaction tests must remain green afterward.
Performance evidence includes raw frame counts and trace slices rather than a single average FPS.
Simulator improvement does not establish a Redmi result, and unavailable real-device/login credentials are recorded as remaining release validation rather than fabricated success.

## Implementation Details

- Preserve the shared markup contract and recursively render children.
- Keep completed-state decoration compatible with nested formatting.
- Avoid new WebViews for math/code.
- Bound caches and cancel stale async render work.
- Keep accessibility, theme and large-font behavior in the renderer contract.
- Optimize demonstrated hot paths while preserving row identity and virtualization.
- Persist any dependency fix and its pin; do not ship an ignored local patch.
- Keep source/comments in English and retain unrelated user work.
- Update the plan after every phase with RED/GREEN and measurement evidence.

## Question

No user answer is required to start implementation.
Redmi hardware, authenticated two-device sync and production signing remain separate release-validation inputs when available.
The exact performance mechanism is deliberately trace-driven; any change of approach and measured outcome will be recorded here.

---

## Current status

Implementation and emulator verification are complete. The [result report](005-android-release-quality_report.md) retains the separate formal Release gaps.

- Rich text: eight real-wire instrumentation cases pass after expected content/accessibility failures. Formula/code: eight layout/pixel/span/contrast cases pass after expected failures.
- The accepted performance fix removes repeated JSON parsing/serialization and historical node-kind accumulation. It reads existing retained backend data and applies the decoded batch. Nine backend contract tests pass on both original and new implementations; this performance refactor preserves behavior rather than inventing a failing functional test.
- LUI change `1edf12691bcd78ee65b52355d5c2a35f4194524c` is published and pinned in `logseq.opam`. The same source tree was cherry-picked onto the current remote main and pushed normally as `e33a152120dff61a65217130d9479f0e974fde47`. Only `LuiBackend.kt` changes: a retained-kind query and public access to the existing decoded-batch method. No view/layout/style changes.
- Release pipeline CPU medians for the same 50-batch native stream/core: original 492.38ms, new 348.09ms, reverse original 1007.37ms (12 samples and six warmups each). The first pair is about 29% lower; host/ART/GC variance prevents a universal percentage or whole-app speed claim.
- Controlled Release scrolling with identical content and native libraries: original 93/743 janky frames (12.52%), new 59/949 (6.22%). Some rounds still miss the suggested budget; retain them as counterevidence.
- Final frozen integration APK, build revision `2ad7d02e` plus this task's workspace changes: 27/993 janky frames (2.72%), with three rounds at 3.40%, 2.08%, 2.70% and P95 26/23/21ms. All meet the emulator budget. This package has a newer core than the controlled pair, so do not attribute the entire difference to this optimization.
- Final default-native Debug/AndroidTest/Release build gate succeeded. Android JVM tests: 54 passed. Final frozen APK instrumentation: 17 passed, including the 10,000-row navigation/virtualization case. Measurement/seed tests: 10 passed. Existing E2E runner suite passes.
- Measurement rejects launch timeouts, dialogs, wrong foreground, zero frames and unchanged lists. Checkpoint metadata resolves the canonical graph folder. API36 system/ART instability is excluded; final measurements use API33 independently.
- Native `screenrecord` final demo: 24.829056 seconds, 1080×2400 H.264, unedited, no audio. Production screenshot and a frame from actual scrolling were visually checked. Recording is separate from timing.
- The root-menu overlay and candidate app Baseline Profile are withdrawn. No app-specific profile or extra menu/layout mechanism is shipped. Historical experiment logs remain explicitly separated.
- iOS layout/styling/source and its LUI pin are unchanged by this task. Concurrent Apple/shared-core/editor edits belong to other work and are preserved.

Redmi hardware, real authenticated editing/sync/attachments, Chinese IME and production signing remain formal Release validation gaps. Emulator completion does not assert that the Redmi issue is resolved.

---

### Agent Guide 005 Android Release Quality Report (`docs/agent-guide/005-android-release-quality_report.md`)

# Android 修复与性能复测报告

日期：2026-10-21 初测，2026-10-22 续测。状态：修复、优化与模拟器复测已完成；正式 Release 验收仍有下述缺口。  
对应[执行计划](005-android-release-quality.md)及[初测 Android/iOS 对照](004-android-ios-release-qa_report.md)。证据沿用本轮 `2026-10-21` 目录。

## 结论

**正文漏显已修复，块公式排版和代码展示已补齐；日记更新管线移除了重复 JSON 处理及不断积累的历史节点元数据。最终固定包在 API33 的三轮复核达到本轮建议预算，仍需真机与真实使用闭环完成正式 Release 验收。** 红米未接入，不能声称其卡顿已经解决。

本轮性能改动只涉及 Android Kotlin；没有修改 iOS 布局、样式或其依赖版本，也没有增加全局浮层、调度器或新的后台队列。此前收益不确定的菜单浮层和 app Baseline Profile 实验均已撤下。

## Android 与 iOS 的已测差异及修复

| 初测 Android 问题 | 本轮结果 | iOS 对照及验证边界 |
| --- | --- | --- |
| 粗体、引用正文消失 | 递归读取共享 markup 的 `children`；保留嵌套格式、链接、Unicode 与完成状态 | 补齐原先 iOS 可显示而 Android 漏显的正文；8项真实渲染/点击/编辑动作测试通过 |
| 公式只是等宽 TeX 文本 | 离线排版上下标、分式和根号；非法表达式可读回退，支持深色与字体缩放 | 块公式功能缺口已补齐；未宣称所有 TeX 命令与行内排版完全等价 |
| 代码无语言标签和高亮 | 显示语言标签和彩色 spans，保留空白、支持横向滚动；浅/深色实际 token 对比度达到4.5:1 | Swift 已实测；未知语言保留可读原文，不宣称所有语言均完成验收 |
| Material 与 iOS 材质/工具栏不同 | 保留 Android 的平台风格 | 合理的平台差异，未以性能优化改变 iOS 样式 |

公式/代码合计8项仪器测试覆盖实际布局、像素、文本 spans、更新及主题切换。公式与高亮计算在有限并发的工作线程执行，缓存有容量上限，没有新增 WebView。依赖和字体许可通知随 Android assets 收录。

最终包[截图](android-release-quality/2026-10-21/android-final.png)与[原生录屏](android-release-quality/2026-10-21/android-final-demo.mp4)见证据目录；录屏为 Android `screenrecord` 原始输出，24.83秒、1080×2400、无音频。iOS 内容对照沿用[初测截图](android-ios-release-qa/2026-10-21/ios-rich-content.png)，本轮没有修改其渲染器。

## 实际定位到的性能问题

Simpleperf 的真实滚动调用栈中，patch 处理约占该次 App CPU 样本的3.53%，不是全部卡顿来源。进一步录下50批 native 日记窗口 patch（8,774,118字节），包含22,609次创建节点、1,522次整棵子树移除，没有逐个节点删除操作。

旧过滤器只在逐个删除时清理节点类型表，整棵子树移除后仍保留历史节点记录，并在每批更新时复制它们；同时执行 JSONObject 解析、重新序列化，再由 LUI 再次解析。

修复直接复用 backend 的现有节点信息及已解析 batch，只保留仍存活的忽略属性。布局与样式规则保持原样，性能部分产品代码净减少十余行。LUI 仅增加节点类型查询，并公开原有的 decoded-batch 入口：[提交 `1edf1269`](https://github.com/logseq/lui/commit/1edf12691bcd78ee65b52355d5c2a35f4194524c)，已在 `logseq.opam` 固定。同一源码树已在保留远端提交后推入 LUI `main`：[提交 `e33a1521`](https://github.com/logseq/lui/commit/e33a152120dff61a65217130d9479f0e974fde47)；App 保持已测试的固定 revision。该提交不含 Swift、OCaml 或布局代码变更；Apple package 继续使用原 revision。

### Release 管线 CPU 对照

在非 debuggable Release 包中，直接回放同一份真实 native patch 流；每组6次预热、12次计时，使用 `Debug.threadCpuTimeNanos()`，中位数按原始样本计算。新旧 core SHA 与 patch 流 SHA 均相同。此测试不包含 Compose 绘制，不能换算成 FPS 或整页提速比例。

| 顺序 | 实现 | 50批处理 CPU 中位数 |
| --- | --- | ---: |
| A | 原管线 | 492.38 ms |
| B | 新管线 | 348.09 ms |
| A 反向复测 | 原管线 | 1007.37 ms |

首组对照减少约29%；反向复测原版仍更高，但也显示了明显的宿主/ART/GC波动，不将这个百分比作为跨设备保证。原始样本、共同输入与校验值：[汇总](android-release-quality/2026-10-21/patch-pipeline/summary.json)、[原版](android-release-quality/2026-10-21/patch-pipeline/release-before.json)、[新版](android-release-quality/2026-10-21/patch-pipeline/release-after.json)、[反向复测](android-release-quality/2026-10-21/patch-pipeline/release-before-2.json)。

### 相同内容的 Release 实际滚动

AVD `logseq_release_qa_33`：API33 Google APIs arm64、1080×2400、60Hz、4核、2GiB、host GPU、headless，关闭网络。两端都已修复富文本和公式/代码，使用同一100篇日记×8blocks的数据库，不含 WebView；Kotlin差异只在 patch 管线的两个文件。原生 core/JNI/graphics 库逐字节相同。均重置 ART 状态、安装各自依赖提供的 profile，再编译 speed-profile；没有全量 speed 编译或候选 app profile。

每轮向下4次、向上4次，各400ms。工具检查 App 前台、无系统弹窗、非零帧与真实列表移动。采样与本任务构建、截图及录屏分开；共享 Mac 上的其他任务仍可能影响结果。

| 轮次 | 原管线 janky/总帧 | 原管线 P95 / P99 | 新管线 janky/总帧 | 新管线 P95 / P99 |
| --- | ---: | ---: | ---: | ---: |
| 1 | 16/326（4.91%） | 32 / 48 ms | 14/332（4.22%） | 28 / 53 ms |
| 2 | 23/252（9.13%） | 38 / 150 ms | 16/300（5.33%） | 40 / 150 ms |
| 3 | 54/165（32.73%） | 150 / 300 ms | 29/317（9.15%） | 53 / 89 ms |
| 合计 | **93/743（12.52%）** | — | **59/949（6.22%）** | — |

这是此组样本的系统 janky-frame 比例，不是平均 FPS。新管线后两轮仍未达到计划建议的 jank<5%、P95<32ms，P99仍达150ms；不能据此签字“卡顿已解决”。[before JSON](android-release-quality/2026-10-21/patch-ui-before/android-performance.json)、[after JSON](android-release-quality/2026-10-21/patch-ui-after/android-performance.json)、[相同原生库校验](android-release-quality/2026-10-21/patch-ui-after/controlled-apks.json)。

最终固定包（build revision `2ad7d02e`，包含本轮 Android 工作区改动）重建了当前 native core，并单独复核：

| 最终包轮次 | janky/总帧 | P95 / P99 |
| --- | ---: | ---: |
| 1 | 11/324（3.40%） | 26 / 73 ms |
| 2 | 7/336（2.08%） | 23 / 30 ms |
| 3 | 9/333（2.70%） | 21 / 27 ms |
| 合计 | **27/993（2.72%）** | — |

这三轮均满足本轮建议的 jank<5%、P95<32ms。首次 P99 仍达73ms，样本也不足以代替真机或长时间验收。最终包 core 与控制组不同，**不能把12.52%到2.72%宣传为管线修复的提速幅度**；管线对照仍以前面的相同 core 数据为准。[最终包 JSON](android-release-quality/2026-10-21/final-integration-performance/android-performance.json)、[APK校验及版本](android-release-quality/2026-10-21/final-integration-performance/apk.json)。

## 验证结果

- Android app JVM 单元测试 **54项通过**，包含9项真实 backend 过滤契约测试；同样9项在原实现上先通过，确认重构保留行为。
- 富文本8项、公式/代码8项及10,000行大列表导航1项，合计 **17项仪器测试**，在最终固定 APK 上全部再次通过（[日志](android-release-quality/2026-10-21/patch-pipeline/final-frozen-ui.log)）。大列表检查可见行有界、滚到9990、返回位置、搜索焦点及输入栏。
- 测量有效性8项、图谱定位/种子2项和既有 E2E runner shell suite通过。已修正写入过期图谱目录的问题，拒绝弹窗、错误前台、未移动和0帧的无效样本。
- 当前 core 的 Debug、AndroidTest、Release 构建已成功；曾有一次增量 APK 打包失败，重试及完整构建 gate 随后成功，失败日志保留，没有计为产品测试失败或成功产物。

详细 APK/core 校验值、构建及测试日志见[证据说明](android-release-quality/2026-10-21/README.md)。CPU采样遵循 [Android Simpleperf 文档](https://android.googlesource.com/platform/system/extras/+/master/simpleperf/doc/android_application_profiling.md)。API36 的系统/ART启动异常及无效样本单独保留，未与 API33 合并计算优化幅度。

## 距离正式 Release 仍缺什么

1. **红米性能验收**：用同一 graph 验证首次与持续滚动、输入延迟、长时间使用和恢复。本次模拟器预算通过，但仍有首次长帧；不能把剩余问题都归因于宿主，更不能保证红米的改善幅度。
2. **真实使用闭环**：真实登录下的编辑落盘/重启保留、双端同步、附件往返，以及中文 IME。本轮离线 fixture 使用虚构 token 与关闭的 localhost 地址，不替代这些验收。
3. **发布工程**：正式签名及分发包验证。本机签名只用于测试，不是生产发布签名。

本轮完成的范围是确证显示缺口、追踪并减少真实滚动更新中的重复工作，以及可复验的前后测试。以上仍未验证或未达标的环节继续作为正式 Release 差距保留。

---

### Agent Guide 006 Android M3 Device Quality (`docs/agent-guide/006-android-m3-device-quality.md`)

# Android M3 and Device Quality Implementation Plan

Goal: Make the LUI Android backend follow Material 3, align composer behavior with iOS, and reduce measured interaction stalls in the emulator.

Architecture: Keep shared product semantics and native platform renderers.
Android controls use official Material components and defaults; Apple rendering remains unchanged.
Fix measured causes without introducing a second menu manager, renderer or scheduling framework.

Tech Stack: Kotlin/Compose Material3, OCaml/LUI, Android instrumentation, Perfetto, physical Redmi.

Related: Builds on [005-android-release-quality_report.md](005-android-release-quality_report.md).

## Problem statement

The user observes nonnative composer attachment/status menus and continuing interaction stalls on a physical Redmi.
Component imports alone are not conformance evidence.
The migration target is the current Material 3 Expressive specification for all Android controls, as explicitly requested by the user.
Baseline components alone do not satisfy this target.
Composer menu nodes are mounted/unmounted through shared model state, interrupting native dismissal transitions.
Several LUI defaults intentionally mirror Apple typography or chrome and require an Android-wide audit.

Root workspace: `/Users/tiensonqin/Codes/projects/logseq-chat`.
LUI source: `/Users/tiensonqin/Codes/projects/logseq-chat/duniverse/lui`.
Preserve all concurrent work and the phone's graph, authentication, draft and attachments.
The user subsequently requested emulator-only interaction testing, then explicitly requested installation on the physical device.
Install and launch the validated preview on the Redmi, preserving data; continue automated interaction tests in the emulator without asking the user to change security settings.
Earlier physical-device traces/screenshots containing graph content stay local under `/tmp/logseq-redmi-qa-20261022`.
Only sanitized aggregate results belong in repository reports or upstream commits.

## Testing Plan

Add the complete menu regression set before implementation.
Use real Compose rendering and the real native bridge for composer cases.
Test entry/exit transitions, single-selection dismissal, selected-state persistence, disabled actions, outside/Back dismissal and reopening.
Validate draft preservation and focus/keyboard behavior against the existing iOS behavior without sending real captures.
Run write/action tests on the emulator, with physical-device checks confined to reversible navigation and menu opening unless a later scenario requires actual edits.

Measure the installed baseline on the physical device before replacing it.
Separate menu, keyboard and scrolling scenarios, retain frame deadlines/CPU scheduling traces and thermal/display state, and repeat the same scenarios after changes.
Do not interpret mixed lifetime gfx statistics as a controlled benchmark.
Do not compile or record video during timing runs.

For changes to shared views, run `rtk proxy opam exec --switch=5.5.0 -- dune build @shared/native/runtest` serially, then rebuild the actual Android core.
For Android UI, assemble matching Debug/test APKs and run the focused instrumentation class on `emulator-5556` before broader retained regressions.
Do not run `swift test`.

NOTE: I will write *all* tests before I add any implementation behavior.

## Work checklist

- [x] Read M3 overview, specifications and guidelines in a JavaScript-capable browser.
- [x] Inspect current composer and LUI rendering paths.
- [x] Confirm the physical Redmi is connected and collect initial diagnostics.
- [ ] Capture repeatable baseline menu/keyboard/scroll evidence in the emulator.
- [x] Automate interaction tests in the emulator and requested installs/captures on the phone; no user assistance required.
- [x] Write all failing behavior regressions for the first menu batch and verify RED.
- [x] Replace composer model-mounted menus with retained native menu triggers on Android only.
- [x] Preserve native dismissal transitions, checked/disabled semantics and component feedback for menus.
- [x] Verify GREEN and retained content/navigation regression gates for the validated batches.
- [ ] Audit LUI Android defaults across typography, buttons, fields, menus, dialogs/sheets, selection controls, lists and navigation against M3.
- [x] Record baseline versus Expressive API availability and the actual dependency/SDK constraints before any upgrade.
- [ ] Validate the official SDK/dependency upgrade and migrate controls to the current expressive APIs.
- [ ] Fix confirmed default/interaction mismatches with behavior tests, avoiding Apple-specific values as Android defaults.
- [ ] Diagnose emulator interaction stalls from trace and apply the smallest evidence-backed optimization.
- [ ] Repeat emulator measurements and install the validated package in the emulator.
- [ ] Publish tested LUI fixes to main, update the app pin, and report actual results plus any remaining gaps.

## First implementation batch

Validated progress on 2026-10-22:

- The first preview Release was installed over the existing Redmi app without clearing data. Android reported `Success`; launching the activity reported `Status: ok`.
- The next Android renderer batch now uses official Expressive `DropdownMenuPopup`, `DropdownMenuGroup`, shaped menu items and `SelectableDropdownMenuItem`. Retained composer menus preserve native dismissal transitions and native single-selection semantics.
- Checkbox, switch and radio labels participate in one native selection target. Tabs use `PrimaryTabRow`/`Tab` and follow model selection updates.
- The combined composer, Material defaults and selection/tab instrumentation run passed all 19 tests in 68.207 seconds on the emulator. JVM tests passed all 57 cases. These are behavior gates, not performance benchmarks.
- The user requested another reinstall. The next batch Release passed packaging/signature validation and was installed over the Redmi app with `Success`; launching returned `Status: ok`. The native core matches the Debug APK used for the 19 behavior tests. Artifact identity and receipt: `android-m3-quality/2026-10-22/expressive-apk.json`.
- Remaining component defaults/modern APIs and controlled Release performance measurements are still pending.

Next validated native-surface batch:

- Nine native-surface tests and fourteen typography/default tests first reproduced thirteen real failures; the complete focused gate now passes 35 tests in 96.569 seconds, including the previous composer and selection controls.
- The drawer uses `ModalNavigationDrawer`/`ModalDrawerSheet`, native motion, scrim, accessibility dismissal and predictive Back. Its settled native state emits a single model event; model updates animate without echo events.
- Popovers use the official popup window rather than inline layout. Buttons use Expressive shapes, and icon-only variants use their corresponding native icon buttons. Lists use current interactive/selected APIs.
- Relative fonts use inherited `em` or Material body `rem` sizes. Semantic headline weights follow the host typography; explicit semibold styling remains supported. Android `glass`/`bar` defaults resolve to opaque Material surfaces.
- The 17 retained rich text, math/code and 10,000-row outliner regressions pass in 36.203 seconds. JVM tests still pass all 57 cases.
- A new signed Release was installed on the Redmi at the user's request; startup returned `Status: ok`. See `android-m3-quality/2026-10-22/native-surfaces-apk.json`.
- Raw logs, profiles, captures and replay dumps remain local and are ignored by Git. Small JSON results, Markdown reports and reproduction scripts remain visible. The root README and QA READMEs are explicitly verified as not ignored.
- Still pending: remaining component API/default review, controlled Release timing and CPU/frame traces, final iOS isolation audit, and upstream publication. No claim of completed performance optimization is made from behavior tests or a single launch.

Latest verification and steering:

- The user reiterated that Android should use native components and native interaction conventions. Preserve functional parity with iOS while keeping platform-specific presentation independent.
- The signed native-surface Release completed a three-run emulator measurement with no concurrent builds or recording. Scroll samples were 5/334 (1.50%), 10/308 (3.25%), and 6/332 (1.81%) janky frames; aggregate 21/974 (2.16%). P95 was 23/22/22 ms. Idle produced zero frames. Results are in `android-m3-quality/2026-10-22/native-surfaces-release/android-performance.json`.
- The earlier baseline was 22/973 (2.26%). This small difference does not establish an improvement, and the native core differs between those artifacts. CPU/frame tracing and a controlled optimization comparison remain required.
- Native accessibility actions completed the requested real Redmi screenshots in 10.952 seconds: main screen, drawer, attachments and status menu. No draft text or selection was changed. Private screenshots remain under `/tmp/logseq-redmi-m3-*.png` and are not committed. The phone was returned to a normal app launch afterward.
- The initial Compose-rule screenshot probe interfered with normal startup on the physical device and was stopped; it is not a product pass. The replacement driver uses platform accessibility actions with no Compose rule, activity replacement or input-injection permission. Its asynchronous drawer-close wait was corrected before the successful run.
- The user asked whether a complete native M3 composer exists. Official Material provides input fields and action controls, and Google's Jetchat sample composes its own input area. Our composer still has a shared custom capsule, borderless `BasicTextField` and Apple-oriented send styling; this is an explicit remaining migration item. Implement an Android-only composition of Material input/actions while preserving draft, attachments, status, IME/focus and outside-dismiss behavior. Add behavior tests first and leave the iOS branch intact.

Composer references:

- https://developer.android.com/develop/ui/compose/text
- https://developer.android.com/develop/ui/compose/text/user-input
- https://github.com/android/compose-samples/blob/main/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/UserInput.kt

Files:

- `shared/src/logseq/view_composer.ml`: Android attachment and task-status triggers own retained menu metadata; keep iOS branches unchanged.
- `duniverse/lui/platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt`: official trigger controls, native open/close state and single-action dismissal without waiting for the model roundtrip.
- `android/app/src/androidTest/kotlin/com/logseq/app/AndroidComposerMenuTest.kt`: native-bridge composer transitions/selection/focus and actual backend menu interaction boundaries.
- `shared/test/logseq/app_test.ml`: only add targeted shared behavior coverage, preserving existing concurrent edits.

The native menu owns visibility; the shared model owns chosen status, attachment services, capture contents and persistence.
A single selected item emits one action.
Disabled items emit no action and leave the menu open.
Outside/Back dismissal must not activate a background control.
Transitions use the official Material component rather than manually copied durations or a root overlay.

## Platform-wide review

Inventory every interactive LUI kind and map it to its Material component, default tokens and interaction contract.
Keep explicit app styling distinct from renderer defaults; do not ignore authored layout values globally.
Use Material typography/color/shape/motion and touch targets by default, with inherited custom typography as an explicit override.
Treat popover-as-inline-column, Apple-size typography defaults, missing selected semantics and fixed native-control dimensions as audit candidates, not automatically verified defects.

The original Material3 1.4.0 dependency provides baseline menus.
Official Material3 1.5.0-beta01 includes Expressive menu APIs but declares minCompileSdk 37 and Compose 1.12.0.
The original app compiled with SDK36 and LUI standalone with SDK35.
Official SDK37.0 has now been installed and the build uses the explicit minor API 0 DSL, Compose BOM 2026.09.00 (UI 1.12.1), and Material3 1.5.0-beta01.
Keep targetSdk 36 for this component migration.
The standalone LUI AGP minimum moves from 9.1.0 to 9.1.1 for SDK37.0 support.
An Expressive upgrade therefore needs compatible SDK/dependency validation and regression checks; do not silently force incompatible dependencies or hand-copy new widgets.

References:

- https://m3.material.io/components/menus/overview
- https://m3.material.io/components/menus/specs
- https://m3.material.io/components/menus/guidelines
- https://developer.android.com/jetpack/androidx/releases/compose-material3
- https://developer.android.com/develop/ui/compose/components/menu

## Testing Details

Tests exercise displayed native controls, semantics, callback counts, actual state changes and retained transitions.
Trace comparisons use the user's actual graph and report the physical device separately from the earlier emulator study.
A menu package change must not inherit an untested core revision without rebuilding and identifying the artifact.

## Implementation Details

- Android defaults follow M3; iOS continues using native Apple behavior and styling.
- Composer actions and outcomes match across platforms.
- Keep menu visibility in the native control, product state in the shared model.
- Preserve system back/outside handling and keyboard/draft behavior.
- Prefer official components over custom imitations.
- Remove duplicated work only after profiling confirms it.
- Keep source/comments English and changes simple.
- Push only tested LUI changes, with no private device evidence.

## Question

Use emulator automation for editing tests and performance comparisons, with no manual assistance.
The user subsequently authorized physical-device installs and screenshots. Preserve all phone data;
use read-only capture and accessibility navigation there, and do not request a security-setting change.
The previous LUI main push authorization remains valid.
If an official component needs an incompatible toolchain, report the exact constraint and keep progress on independent fixes.

---

## Composer migration verification

- The new real-native-bridge composer gate ran seven emulator tests before implementation. Six failed for the intended product differences: 36dp input, unbounded 288dp multiline growth, an invisible focus button, custom black Send fill, and legacy camera/Send sizing. Unicode draft/status retention already passed.
- The Android branch now composes the official Material input and filled/tonal icon buttons. Long input is bounded to five visible lines, and empty toolbar space is a spacer. The Apple capsule and Apple-specific control values are retained in their existing branch.
- The obsolete test requiring Android to copy iOS chrome was replaced with an actual capture contract check: Unicode/multiline input, dismissal/reopen, enabled state, trimmed submission and draft clearing. The existing iOS layout/style contract remains unchanged.
- The first complete 59-case emulator run passed 58 cases. The remaining camera assertion incorrectly required a 40dp semantic slot even when the parent reserves 48dp; it now verifies the native tonal color and 48dp touch target. The corrected check was verified failing against the previous APK and passing against the composer APK. All seven composer tests pass.
- The shared gate passed 897 cases for the composer batch, including the existing Apple composer layout/style checks. Final combined toolbar validation and screenshots are pending.

## Editor and selection toolbars

- The user explicitly added editor and selection toolbars to this migration. Horizontal LUI toolbars now use official `HorizontalFloatingToolbar`, retaining a horizontally scrollable action area and the pinned final action. Shared Android code delegates container size, shape and color to Material; Apple toolbar branches remain unchanged.
- Four failures were reproduced before implementation: both toolbar containers used custom row metrics, and forward/reversed native text selections collapsed after a shared caret acknowledgement. A local equality check now distinguishes that acknowledgement from an external caret request, preserving the native selection without adding a second selection manager.
- The toolbar test fixture verifies native-bridge command delivery rather than assuming a bare bridge executes host effects. Overflow tests check the pinned action remains stationary and emits exactly one typed `hideKeyboard`/`unselect` command. External caret requests remain covered.
- The shared gate passes 898 cases, and all 57 JVM tests pass. Fresh native APK and the complete combined emulator instrumentation gate are pending.
- Native reference: https://developer.android.com/reference/kotlin/androidx/compose/material3/HorizontalFloatingToolbar.composable.

## Profile-guided investigation

- A 16-second `simpleperf` task-clock profile of the native-surface Release collected 1,425 samples with zero lost samples during automated scrolling, with no concurrent build. Interpreter dispatch (`ExecuteNterpImpl`) accounted for 7.58% self time; Compose layout and prefetch dominated inclusive stacks. JNI polling was not a major sampled hotspot.
- The current release configuration explicitly disables R8, while referring to a nonexistent `proguard-rules.pro`. Official Android guidance recommends enabling release optimization. Audit native lookup entry points, then compare optimized and unoptimized builds with an identical core and source snapshot before claiming a speedup.
- References: https://developer.android.com/develop/ui/compose/performance and https://developer.android.com/topic/performance/app-optimization/enable-app-optimization.

## Remaining component audit

This inventory is based on the current Android renderer, not on component imports alone.

| Surface | Current evidence | Remaining work |
| --- | --- | --- |
| Buttons and icon buttons | Official Expressive shapes/variants; native semantics, disabled state, touch targets and press shape tested | Broader app theme review |
| Checkbox, switch, radio and tabs | Native components; label interaction, single callback, disabled/selected state tested | Dark mode and large text review |
| Menus | Current popup/group/item APIs; retained transitions, Back/outside, action counts and radio semantics tested | Full anchor/alignment/offset contract; landscape and IME placement; detached inline fallback |
| Drawer | Native modal drawer, scrim, motion and accessibility dismissal tested | Actual-device predictive Back capture |
| Popover | Native popup and Material surface; Back tested | Full wire anchor contract |
| Composer | Material field and action buttons; draft/status/Unicode/outside preservation and five real-IME flows tested; combined gate passes | Release keyboard timing and landscape coverage |
| Editor and block-selection toolbars | Official horizontal floating toolbar; real swipe reaches overflow with pinned final action; combined gate passes | Vertical toolbar remains a custom column |
| Native text selection | Caret acknowledgement preserves noncollapsed selection; external caret requests tested | Actual platform selection-menu/clipboard scenario |
| Text entry and slider | Material controls are present | Current state APIs and IME/composition audit; slider uses deprecated value API |
| Tooltip | TooltipBox anchor exists, but content is plain Text | PlainTooltip surface and current position API; long-press/dismiss behavior |
| Dialog and sheet | AlertDialog and ModalBottomSheet | Focus/keyboard/dismiss gates; replace deprecated sheet state API |
| Card, toggle and toast | Card uses stack layout; toggle maps to FilterChip; toast uses Card | Use appropriate official Material surfaces/controls and test actual semantics/dismissal |
| Bottom navigation and progress | Native NavigationBar and progress indicators | Focused selection/disabled and current defaults review |
| App defaults | Renderer uses Material typography/surfaces | Remove remaining Apple-derived app semantic-color overrides and editor text-style overrides with behavior coverage |

Layout/data primitives (rows, columns, lazy lists, tables, images, media and custom document structure)
do not acquire a new interactive control merely to resemble a Material component. Preserve their
authored layout and behavior while checking the Material defaults they inherit.

## Screenshot-driven spacing correction

- The user requested a smaller bullet-to-text gap and a common vertical guide with the page title.
- Six geometry/interaction tests were added first. Four reproduced real failures: a missing priority still reserved about 7dp, affecting reading, editing and priority removal, and the title edge was 7.5px away from the bullet center at emulator density.
- A missing priority now mounts no view. The Android journal heading inset uses half of the 24dp bullet slot, keeping its leading edge on the bullet center line. Native bullet touch targets and nested indent steps are covered.
- The shared gate passes 898 cases. The aligned native build passes all 72 combined emulator cases in 174.327 seconds. Updated captures are `/tmp/logseq-m3-aligned-editor-toolbar.png` and `/tmp/logseq-m3-aligned-selection-toolbar.png`.
- A broader 66-case run initially passed 64 cases. The other two assertions were corrected to respect native selection-direction normalization and to use actual horizontal swipes instead of `performScrollTo`, whose clipped target bounds were zero. All seven toolbar cases then passed in 34.262 seconds, including visible overflow and one typed command per action. This is behavior verification, not a performance benchmark.

## Composer menus and the real software keyboard

- The requested parity is to retain an already visible keyboard for both status and attachment menus, and to leave an already hidden keyboard hidden.
- Installed-app instrumentation reproduced the keyboard closing for both menus, outside dismissal and status selection. These checks use `dumpsys input_method` and real input events on the dedicated `uiregression` emulator graph; they do not mock the input method.
- The Android popup now uses the native `FLAG_ALT_FOCUSABLE_IM` policy alongside outside-touch observation. The popup retains native focus, Back handling, accessibility focus and exit transitions, while avoiding becoming an input-method target. No keyboard hide/show cycle or global menu controller was added.
- Native touch-modal popups intentionally move accessibility focus away from the underlying input while open. Tests check the real IME and menu geometry during that interval, then verify the input focus, position, draft, caret and continued typing after dismissal.
- Run installed-app methods in separate instrumentation invocations so each gets a fresh native host. The opt-in `verifyComposerIme=true` checks reject physical devices and graphs other than `uiregression`; they restore the draft without submitting content. Combined class execution exposed activity/native-host teardown races and is not a passing gate.
- All five installed-app keyboard flows pass. During each open menu, screenshot samples verify the actual keyboard remains visually unchanged, in addition to input-method service state. Status selection preserves the caret as verified by inserting a real key at that position; direct accessibility selection indexes were not a reliable native-platform assertion.
- The current native UI regression passes all 72 cases in 205.323 seconds. The Android host JVM suite passes 57 cases; standalone LUI Android passes 29 cases, including its Paparazzi run, with compiler warnings retained. These are functional gates, not performance measurements.
- The validated Android renderer batch was pushed to LUI main as `ab4c1e2d20d9b3ef9caa7451c5b5ecceb0e6d9a3`; `logseq.opam` now pins it. Apple renderer sources and the Apple package manifest were not changed by this batch. The remaining component audit above remains open.


## Block editor task status feedback

- Installed-app automation reproduced a visible-feedback failure: Todo and Doing updated the accessible label and persisted graph state, but the settled toolbar icon had exactly the same strokes. The regression test failed with zero differing icon pixels before the fix. It ignores background/ripple color changes rather than accepting any screenshot difference.
- The Android task button now derives its icon and foreground from the edited block's current status, using the same status helpers as composer and outline rows. The existing core cycle, native button, toolbar geometry and Apple branch remain unchanged.
- The new installed-app test passes the None → Todo → Doing → Done → None cycle, checks an actual settled icon change, keeps the real IME open and the toolbar anchored, and verifies Done after leaving and reopening the editor. It restores the original block status.
- The persisted-graph core regression also checks the cycle, unchanged Unicode block text and status after reopening the graph. The shared gate passes all 900 cases. The synthetic performance graph used in earlier QA lacked canonical task entities; the emulator fixture was backed up and rebuilt with canonical schema before these UI results. No production migration or phone data reset was added to address that fixture defect.
- Actual post-fix captures: `/tmp/logseq-block-task-todo-after.png` and `/tmp/logseq-block-task-doing-after.png`. The tested APK is `/tmp/logseq-block-task-fixed-debug.apk`, SHA-256 `824863b1a074c8079a292f70edfaa20bfe9662bfa9042b15d1fcedd30c556024`.
- Follow-up emulator regressions pass all five real-IME composer flows and all seven native-bridge toolbar cases. The same frozen APK installed successfully on Redmi `4a23a2e9` using `adb install -r`; existing phone data was retained. These are functional checks, not a new performance measurement.

---

### Agent Guide 007 Android Functional Parity (`docs/agent-guide/007-android-functional-parity.md`)

# Android Functional Parity Implementation Plan

Goal: Complete the six confirmed Android functional gaps, with one tested commit per feature.

Architecture: Reuse the shared OCaml graph operations and the existing Android runtime, attachment storage, presentation host and native Compose controls.
Keep Apple layout unchanged. The separately requested iOS capture confirmation duration is capped at 1.5 seconds (474d9d6).
Do not resume the paused performance or component migration work.

Tech Stack: Kotlin, Jetpack Compose, Android system intents and scheduling, existing OCaml RPC.

Related: Builds on 006-android-m3-device-quality.md and the functional comparison in this chat.

## Problem statement

The current Android UI implements the principal graph workflows but lacks the iOS image gallery, complete incoming attachment sharing, quick recording entries, coordinated YouTube seeking, Shift+Enter within a block and persistent background sync scheduling.
The existing Android work was committed and pushed first as 88feb9f.
Unrelated Apple changes in this shared checkout are excluded.

## Testing Plan

For each feature, write its happy-path, edge-case and failure tests before implementation.
Observe a genuine behavior failure, implement the smallest change, rerun the focused tests and relevant regressions, inspect the diff, then commit that feature alone.
Use actual Compose gestures, Android intent resolution, real files and system services rather than mock-only assertions.
Use the dedicated emulator graph for any content-changing test.
Never clear or seed the physical phone's user graph.

NOTE: I will write *all* tests before I add any implementation behavior.

## Ordered commits

- [x] Image gallery: add paging, pinch/pan, current-image save/share and Back dismissal in Android's existing presentation host.
  Touch Android runtime/extension registration, a dedicated gallery composable and image storage helpers.
  Verify the selected initial image, real swipe/pinch, current-page action paths, missing images and actual MediaStore writes.
- [x] Incoming shares: register audio, video, PDF, Markdown and generic files, preserving text/images and multiple attachments.
  Touch AndroidManifest.xml and Android app-entry tests.
  Verify package-manager resolution and actual URI import, including text plus attachments and cancellation/error handling.
- [x] Quick recording: register the audio deep link and recording actions for launcher shortcuts, capture widget and a native Quick Settings tile.
  Touch AndroidManifest.xml, shortcuts.xml, AndroidWidgets.kt and the existing entry handler.
  Verify cold-entry routing, one recording request, existing capture/journal routes and OS shortcut/widget/tile declarations.
- [x] YouTube timestamps: retain playback start state per video within the rendered document, show timestamps as actions, and update the corresponding existing player.
  Touch OutlinerBlockContent.kt and rich-content tests.
  Verify visible timestamp actions, matching-player seeking, unrelated-player isolation and invalid data fallback.
- [x] Shift+Enter: preserve a newline inside the edited block while ordinary Enter keeps its split behavior.
  Touch OutlinerEditor.kt and editor instrumentation tests.
  Verify insertion/replacement with Unicode selections, actual keyboard modifiers, regular Enter and composition handling.
- [x] Background sync: register persistent network-constrained system work and reuse the real authenticated core sync path without creating a second UI runtime.
  Touch the application/scheduler/worker and existing authentication/core serialization boundaries.
  Verify unique scheduling, no-session/no-graph behavior, actual native sync against a local endpoint, retry/cancellation and coexistence with the foreground runtime.

## Testing Details

Use `cd android && ./gradlew :app:testDebugUnitTest :app:assembleDebug :app:assembleDebugAndroidTest --max-workers=2 --no-parallel '-Dorg.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=1g'` as applicable.
Run the focused installed instrumentation class on emulator-5556 and inspect its test count and failures.
If shared OCaml changes are necessary, serialize `opam exec --switch=5.5.0 -- dune build @shared/native/runtest` with native mobile builds.
Do not run swift test or edit Dune files.
Record actual commands/results and feature commit hashes below as work completes.

## Implementation Details

- Use Android platform APIs and current existing dependencies before adding a library.
- Keep image and attachment paths under the existing graph/cache/FileProvider rules.
- Share or save the currently selected image rather than the initially opened image.
- Preserve normal capture, journal, text input and asset behavior.
- Keep network sync serialized with the existing native core.
- Commit only feature-owned files and leave concurrent Apple edits untouched.
- Push the resulting commits to main using the existing authorized remote.

## Question

No user action is required for the emulator checks.
Background work follows Android's network, power and scheduling restrictions; it does not promise an exact execution time.

---

## Verification log

- Initial checkpoint: 88feb9f pushed to main; native core gate passed, JVM tests passed, and 14 QA utility tests passed.
- iOS confirmation: 474d9d6 pushed; behavioral RED followed by 900 passing core tests. iOS 1500 ms, Android 2500 ms.
- Gallery: four new instrumentation cases cover swipe, real pinch with pixel changes, unavailable images and byte-exact MediaStore save. Eight rich-content regression cases also pass.
- Gallery entry regression: actual imported siblings exposed missing cache-model paths in the gallery RPC. A real persisted-graph test reproduces the failure; 900 core tests pass after resolving local paths from the existing cache model.
- Gallery installed-app entry check: actual external shares open a four-image gallery, a real swipe advances from 1 / 4 to 2 / 4, and Android Back dismisses it. Screenshot: `/tmp/logseq-gallery-entry.png`. The rebuilt native library includes the core regression fix.
- Gallery feature committed and pushed as 227b174.
- Incoming shares: RED OS resolution for audio, then two instrumentation cases pass across eight MIME types and both send modes. Real FileProvider documents retain their original bytes; missing/empty sources are rejected. An actual implicit PDF share cold-launches MainActivity and imports the exact bytes into the dedicated QA graph.
- M3 gallery follow-up: full-screen native dialog with edge-to-edge system insets, M3 surface/container tokens, TopAppBar and native Save/Share icon actions. All four gallery instrumentation tests pass after the styling change; actual app light-mode capture inspected at `/tmp/logseq-gallery-m3-light.png`.
- Quick recording: three OS integration tests first fail for absent audio route, shortcut, tile and widget action, then pass. Actual emulator Quick Settings tile click cold-opens exactly one existing M3 recording sheet with Stop/Cancel; cancelled without adding a recording to the graph.
- YouTube timestamps: two new rich-content cases reproduce the old duplicate-player/unreadable timestamp behavior. Ten rich-content cases now pass, including a separate timestamp block seeking the existing matching native WebView, retaining both WebView instances and leaving the other video URL unchanged. Invalid timestamps remain readable and disabled.
- Shift+Enter: two real modified-key tests first reproduce structural splits, then all three keyboard cases pass for append, reversed Unicode selection replacement and ordinary Enter/multiline paste. Combined rich-content, keyboard and native editor toolbar suite: 20 passed.
- Background sync: six installed instrumentation cases pass with real encrypted token storage, real native core calls, a real local WebSocket server and a prepared plaintext QA graph. The received server cursor advances from 0 to 1 in the graph checkpoint. Network rejection retries, cancellation releases the core, absent sessions skip, and a held foreground lease blocks cold session replacement.
- Background auth regression: a real refused refresh connection first reproduced credential loss. Transient failures and cancellation now retain refresh credentials; explicit invalid-grant/token responses still clear rejected credentials.
- Cached encrypted graph bootstrap: a boundary regression first fails for an unlocked cached graph, then passes; locked graphs continue to await unlock. This is separate from the plaintext native network integration test.
- System integration: a forced JobScheduler execution binds the real JobService while MainActivity is editing. The composer draft remains unchanged. An OS timeout stops the job, returns it to waiting, and the temporary QA draft suffix is removed.
- Final combined installed suite: 35 passed in 63.766 seconds. JVM suite: 58 passed, zero failures/errors/skips. Shared core: 900 passed after the gallery path correction.
- Reproducible background flow: `scripts/test-android-background-sync.sh emulator-5556` clones only the dedicated QA graph into temporary storage, removes pending operations/errors in that clone, seeds a current journal and runs the six native integration cases. The flow itself passed. It rejects physical device serials and leaves the source graph database unchanged.

## Delivered commits

| Feature | Commit |
| --- | --- |
| Existing Android work checkpoint | 88feb9f |
| iOS confirmation capped at 1.5 seconds | 474d9d6 |
| Image gallery and imported sibling cache paths | 227b174 |
| Incoming audio/video/document shares | f289c63 |
| Unified M3 gallery presentation | 9964f15 |
| Recording shortcut, widget action and Quick Settings tile | dd44309 |
| Existing-player YouTube timestamp actions | d6776fc |
| Shift+Enter newline within a block | f531f4f |
| Persistent native system background sync | This final feature commit |

Android controls added by this plan use Material 3 components; widgets, shortcuts, share sheets and tiles use their native Android system hosts.
Background execution follows Android's network, power and scheduling limits. The minimum periodic interval is 15 minutes, and a locked encrypted graph requires unlocking before it can sync.
No Apple layout or styling changes are included in these feature commits.


## Additional parity pass

Complete four further confirmed gaps in separate commits. Continue using the TDD and emulator workflow above.
No Apple layout changes or paused performance work are included.

- [x] Remote images: use prepareAssetDownload/cacheDownloadedAsset for authorization, graph ownership and decryption; download off the UI thread; show M3 loading and retry controls; open the downloaded file in the gallery.
- [x] Durable incoming shares: persist before delivery, retain stable capture identifiers and attachment bytes across restart, acknowledge only successful core operations.
- [x] Editor settings: apply saved autocorrection/spell-check preferences to the native input connection and verify text and selection survive updates.
- [x] Graph progress: show real download/decode/import/open stages and native import record counts in an M3 progress presentation; dismiss on success/failure and reject stale callbacks.

Remote-image verification: three JVM cases pass for cached access, actual HTTP failure followed by retry with byte-exact caching, and graph-change rejection with temporary-file cleanup. The installed emulator suite passes 15 cases: remote retry/preview, rich content and gallery gestures/actions. Tests were observed failing before implementation.

Durable-share verification: both restart/failure and cancellation/attachment-survival tests first timed out against the in-memory queue, then pass on JVM and the installed emulator. Four installed cases pass with existing MIME/file-import coverage. Shares and attachment bytes are atomically staged under filesDir, stable identifiers survive replay, delivery stays ordered, activity recreation retains the intent token, and only successful graph handling acknowledges the entry. Disk work runs on Dispatchers.IO; lifecycle cancellation retains the queue without reporting an import error.

Editor-setting verification: the native EditorInfo regression first showed AUTO_CORRECT still enabled after saving false. Five keyboard cases and seven toolbar cases now pass on the emulator, including actual input flags, preference changes with an existing selection, Unicode replacement, multiline input and sentence capitalization. The remembered input interceptor restarts only when settings change. Android TYPE_TEXT_FLAG_NO_SUGGESTIONS overrides automatic correction; disabling spelling suggestions follows that native rule. Reference: https://developer.android.com/reference/android/text/InputType#TYPE_TEXT_FLAG_NO_SUGGESTIONS .

Graph-progress verification: download/lifecycle/visible-dialog/native-callback tests first fail without progress. After implementation, 64 JVM tests pass, including actual HTTP bytes, gzip decoding, rejected-download cleanup and lifecycle success/failure cleanup. The combined installed emulator suite passes 33 cases in 31.922 seconds. A real native snapshot import generated from the isolated QA graph emits importing/opening callbacks with a positive, completed record total through the JNI bridge. M3 progress updates visible counts, rejects Back dismissal during import, and closes on completion/failure. Native callbacks are accepted only for the active graph operation; unknown totals stay indeterminate. The prebuilt core exports the existing progress ABI, so only the JNI shim was rebuilt; no shared-core or Apple changes were needed.

Additional feature commits: remote images 0fef9bb; durable shares 22e0fb8; editor settings 3b00134; graph progress in the following feature commit. The next requested UI/UX review is read-only for application code and will produce a separate HTML report with fresh screenshots and explicit verification limits.

---

### Agent Guide 009 Android Ui Review Fixes (`docs/agent-guide/009-android-ui-review-fixes.md`)

# Android UI Review Fixes

Goal: Resolve UX-01, UX-02 and UX-03, adapt Settings and Sync status to Android, and use the Logseq theme for links and action buttons.

Scope: Preserve Apple rendering, layout and interaction. UX-04 is withdrawn as a device defect: the user verified that the iOS device retains its keyboard for both composer menus. Simulator evidence is not representative of that device behavior. Do not resume performance optimization or change concurrent search work.

## Validation plan

- [x] Write all behavior regressions first: normal/rich/quote long press, short tap and reference navigation, collapsed capture width/background/hit target, initial graph sheet actions, settings title/trailing switches, sync error scrolling/actions, and light/dark link/action colors.
- [x] Run the new tests before implementation and record genuine behavior failures.
- [x] Fix native Android layout and gesture ownership locally. Reuse Material components and existing events; do not add a second rendering or theme system.
- [x] Run focused tests, then relevant existing Android regressions and the serialized OCaml core gate if shared view code changes.
- [x] Rebuild native core, install in the dedicated Android emulator and verify real entry paths, keyboard behavior and screenshots. Do not modify phone data.
- [x] Commit separate coherent changes, push the authorized main branches, update the review status and mobile report with fresh evidence.

## Implementation boundaries

LUI Android fixes live under `duniverse/lui/platform/android/lui`. Keep its independent repository commit and update the application pin after its main push. Application view changes are restricted to KotlinHost branches; Android native code owns rich text and theme use. Run no swift test and edit no Dune files.

## Verification log

- RED: 39 installed tests executed; 20 expected failures reproduced the reviewed interaction/layout/color defects. One invalid link-variant fixture was corrected to use the supported text-button variant and independently observed failing on scoped color behavior.
- GREEN: all 39 focused installed tests pass in 48.288 seconds, including long press, accessibility editing, native sheet actions, capture hit regions and theme colors.
- Core gate: 901 tests pass in 43.960 seconds. The old Android settings assertions were updated for the requested flat native preferences and content-sized actions; Apple branches are unchanged.
- LUI and Android JVM tests pass; broad installed regressions and real entry screenshots are next.
- Additional user scope: reproduce and optimize repeated Enter insertion near the start of a 500-block Android page, with actual input timing and a separate change. iOS layout/styles must remain unchanged.

- Final focused run: 51 installed component/renderer regressions pass, including two new trailing-action layout assertions discovered during screenshot review. The opt-in IME cases are executed separately with verifyComposerIme=true.
- Existing broad installed regression run: 61 pass. Shared core: 902 pass in 42.402 seconds. Android JVM: 64 pass; LUI JVM: 29 pass.
- Actual app entry checks: rich-body long press reaches selection and unselect works; the 55%/92% Capture area opens the composer and outside touch dismisses it. Settings title, trailing switches and full-width server input were visually reviewed. Final Graph/Sync screenshots and report publication remain.
- LUI UI/layout commit d09197cbc70a986f4c0b6f43e37f7d6628d9f395 is pushed to main. Android editor reuse is a separate performance change.

## Completed follow-up

- Actual App captures now include initial/keyboard Graph forms, Light/Dark Settings, Sync status, rich-body selection and the collapsed Capture hit target. Settings theme selection exposed an event-order bug: native menu dismissal removed the shared action before Press delivery. LUI now delivers the choice first. Sync label/value rows now honor space-between. Both regressions were observed failing before the renderer fixes and pass afterward.
- UI commit `94fcee3` and follow-up editor/renderer pin commit `eddb347` are pushed to main. LUI Android commits through `e3ed35af33f6c0c1965cd719327ee4dd67c47d0f` are pushed to its independent main branch.
- Final installed regressions: 61 focused/native IME tests pass in 125.687s with verifyComposerIme=true and no skips; 61 additional Material/surface/gallery/share/recording/toolbar regressions pass in 66.193s. Android JVM 64 and LUI JVM 29 pass. Shared core 904 pass in 43.838s.
- Two teardown failures in the first combined run came from restoring draft text before a native menu had completed dismissal. Cleanup now waits for that actual state, and the complete 61-test rerun passes. Historical logs are retained rather than relabeled.
- The HTML report contains 46 selected screenshots and updated status. Browser QA at 390 and 1280 pixels verified no horizontal overflow, decoded images, filtering and lightbox behavior; all 94 local asset/evidence links resolve. Only selected report files are committed; raw test artifacts remain ignored, and README remains tracked.
- Updated the existing owner-private mobile report successfully at https://logseq-mobile-ux-review-20261022.tiensonqin.chatgpt.site. No phone data changed, no Dune files changed, and no swift test ran. Apple sources/layout/styles remain unchanged.

---

### Agent Guide 010 Android Enter Latency (`docs/agent-guide/010-android-enter-latency.md`)

# Android Repeated Enter Latency

Goal: Keep repeated Enter block creation responsive near the beginning of a page with 500 existing blocks, without changing iOS layout or adding a second editor/model.

## Validation plan

- [x] Add an opt-in emulator test that injects actual Enter key events into the installed app, records time until the next editor is focused, and verifies the original text survives repeated splits.
- [x] Run a controlled 500-block fixture before changes and identify the slow stage from real measurements.
- [x] Add focused regressions at the responsible boundary before implementing the fix, including ordering/data integrity and rapid repeated input.
- [x] Apply the smallest root-cause fix. Do not conceal latency with delays or replace native controls.
- [x] Repeat the identical fixture and measurement after changes; report median, p95, worst latency and any environment limits.
- [x] Restore the dedicated QA graph, run relevant regressions, commit separately and update the mobile review with evidence.

Only emulator-5556 and the dedicated UI Regression graph may receive fixture changes. Back up the graph using SQLite backup, restore it in finally, and reject physical device serials. No user action is required.

## Results and implementation

- Same 500-block SQLite fixture, SHA-256 `089d1ce138ae9b77332a464153f3f332c7e2f6061c04201efbe1673e60f77942`. Each round uses 3 warmups, 12 measured Enter inputs, then 20 inputs at 20ms intervals. The driver verifies every persisted split UUID, original text, empty prefix and chain order, and restores the original QA graph in `finally`.
- Baseline: median 117.5ms, nearest-rank p95 / maximum 160ms, 16 editor instances, 24/35 persisted splits. Hardware Enter after: two consecutive passing rounds, medians 91/87ms, maxima 147/126ms, one editor and 35/35 persisted splits. Native IME action after: median 66.5ms, maximum 105ms, one editor and 35/35 persisted splits.
- One run concurrent with the core gate had a 180ms outlier. Final timings run without concurrent builds/tests. These debug emulator samples measure input-to-editable Compose semantics, not complete pixel presentation or Redmi Release performance.
- Swift comparison: the pinned Apple virtual-list renderer preserves one active-editor identity. `apple/Sources/Logseq/OutlinerInlineEditor.swift` applies text/caret in the update pass, retains the first responder and buffers input during handoff. Android now preserves its lazy editor key and source scope, reconciles each handoff in one composition and queues rapid input in order through native keyboard actions/hardware keys. Shift+Enter and multi-line paste remain text.
- Profiling found repeated full-page hydration in the shared split path. Adjacent splits now update the cached page using source/new leaf and dependent labels, with normal cache invalidation/fallback for other operations. Cached results are compared with fresh reads for nested order, references, status labels, other pages and later edits.
- A separate actual-app failure exposed background journal refresh truncating rows to the initial 32. Once insertions moved the editor past that limit, its row vanished and focus jumped to the header. Refresh now retains the active page topology while editing; launch hydration remains bounded. The regression first failed, then passed across 35 insertions and an authoritative content update. This is a shared data correctness fix; no Apple renderer, app source, layout or style was changed.
- Full serialized core gate: 904 passed in 43.838s. Android JVM: 64 passed; LUI JVM: 29 passed. Installed keyboard/parity, native layout and real IME regressions are recorded in the report verification summary.
- LUI changes are pushed to main through `e3ed35af33f6c0c1965cd719327ee4dd67c47d0f`; the application pin is updated. No Dune files changed; no swift test ran.

Application code is committed and pushed as `eddb347`. The detailed report and curated evidence are published to the existing private mobile review; the source fixtures and raw artifacts stay ignored. The complete installed regression reruns pass without skips. Redmi and signed Release performance remain unverified by these emulator measurements.

---

### Agent Guide 011 Query Block Rendering (`docs/agent-guide/011-query-block-rendering.md`)

# Query Block Rendering Implementation Plan

Goal: Render DB graph Query blocks and their live simple or advanced query results on iOS and Android.

Architecture: Execute queries against the same pending-operation DataScript projection as the outline.
Vendor the mobile-safe query algorithms from Logseq commit `a63da5cb7993302f8c58ccf42eaf0083b77cf42b` (dev plus the fixes in [upstream PR #13821](https://github.com/logseq/logseq/pull/13821)), following the existing `Dev_sync` extraction pattern.
Serialize query results with hydrated blocks and render them through shared LUI elements so both native hosts receive the same behavior.

Tech Stack: OCaml, DataScript OCaml, melange-edn-native, LUI, native SwiftUI and Jetpack Compose hosts.

Related: Builds on `002-ocaml-platform-boundary.md` and `003-shared-domain-consolidation.md`.

## Problem statement

The mobile outline currently renders Query-tagged blocks as ordinary blocks.
It does not follow `:logseq.property/query`, execute the referenced definition, or display results.
The upstream dev branch already supplies the DSL compiler, Datalog rule and input handling, and an EDN result-transform evaluator.
The mobile app should reuse those algorithms without importing the complete desktop worker and its platform dependencies.

## Testing Plan

Add `shared/test/logseq/query_block_test.ml` with seeded real graph databases and RPC sessions.
Cover simple tag, reference, property, task and boolean queries, advanced pull and aggregate queries, dynamic context and dates, user and built-in rules, transforms, empty results, invalid definitions, hidden and recursive results, and updates after local and remote changes.
Verify Query blocks execute in snapshots and distant-row hydration, while ordinary blocks remain unchanged.
Exercise the real shared UI on both native host protocols to verify results, navigation, error and empty messages, folding, and progressive result disclosure.
Verify query-result blocks do not recursively mount their own queries.
Run focused tests during development, then `rtk proxy dune build @shared/native/runtest` as the core gate.
Use native platform build checks as appropriate; do not run `swift test`.
NOTE: I will write *all* tests before I add any implementation behavior.

## Data flow

```text
Open graph + pending operations
           |
     projected DataScript DB
           |
Query block -> referenced definition -> simple DSL / advanced Datalog
           |                                   |
           +---------------------- results + errors
                                               |
                         snapshot / content hydration
                                               |
                       shared LUI query result view
                              /              \
                           SwiftUI       Jetpack Compose
```

## Tasks and execution checklist

- [x] Inspect upstream and current outline, snapshot, hydration, and pending-projection boundaries.
- [x] Create the implementation plan and active goal.
- [x] Write all regression and integration tests and observe missing-feature failures.
- [x] Add reproducible upstream extraction in `scripts/vendor-dev-query.py` and mobile adapters in `scripts/vendor-dev-query-platform.ml`.
- [x] Generate `shared/src/logseq/core/dev_query.ml` with a matching interface and pinned source/license provenance.
- [x] Implement `shared/src/logseq/core/query_block.ml` and `.mli` for definition resolution, execution, normalization, and isolated errors.
- [x] Expose the projected database through `shared/src/logseq/core/mobile_graph.ml`, `.mli`, `mobile_session.ml`, and `session_types.ml` / `.mli`.
- [x] Integrate results with `shared/src/logseq/core/rpc_session.ml` snapshots, hydration, and query RPC reads.
- [x] Carry query payloads through `shared/src/logseq/model.ml`, `.mli`, and `response_snapshot.ml`.
- [x] Add shared result rendering in `shared/src/logseq/view_query.ml`, `.mli`, and `view_outliner.ml`, registering modules in `shared/native/dune_modules/*.sexp`.
- [x] Fix upstream query parser defects with endpoint regressions and submit a PR against dev.
- [x] Run focused and full verification, review the diff, and record results here.

Each implementation task starts by running its corresponding failing behavior test.
After implementing the minimum required behavior, rerun the focused test, then refactor and rerun it.
Checkpoint updates report progress while execution continues under the user's explicit goal authorization.

## Behavior and edge cases

| Situation | Required behavior |
| --- | --- |
| Query tag or query property | Resolve the property reference to its definition block. |
| Simple definition | Compile and execute the upstream DSL. |
| Advanced definition map | Execute `:query`, `:inputs`, and `:rules`, then optional `:result-transform`. |
| Scalar, tuple, aggregate, map results | Display readable values without inventing block identities. |
| Block results | Preserve markup and navigation identity; omit deleted, hidden, definition, and self results. |
| Query block in results | Display the block without recursively executing another query. |
| Empty definition | Display a useful empty-definition state. |
| Invalid or unsupported expression | Show a query-local error while sibling outline content remains usable. |
| Collapsed owner | Suppress query-result content. |
| Many results | Initially mount a bounded number and let the user reveal more. |
| Pending or remote edits | Recompute against the current projected DB, including hydrated distant rows. |
| Graph switch | Discard prior query data through the existing graph projection lifecycle. |

## Testing Details

Tests execute real queries over seeded DataScript databases through the session RPC and assert actual result identities and values.
UI tests mount the real shared app, apply its query-bearing snapshot, activate result controls, and inspect navigation and mounted content.
The full core test alias checks regressions in synchronization, projection, hydration, navigation, and rendering.

## Implementation Details

- Keep upstream algorithms reproducibly generated and mobile glue handwritten.
- Reuse the existing native EDN and Transit codecs.
- Adapt native platform helpers without adding browser or worker dependencies.
- Evaluate read operations against the pending projection without mutating the graph.
- Include source, kind, rows, and error state in each query payload.
- Use block JSON with markup for entity results and explicit textual cells for other values.
- Keep result navigation independent of outline editing and selection.
- Preserve bounded outline hydration and progressive result mounting.
- Surface unsupported custom Hiccup views explicitly; do not execute browser rendering code.
- Preserve pre-existing workspace edits and avoid unrelated changes.

## Question

No user input is required to begin.
Editable query builders and full desktop view customization are separate work.
Chrono-node natural-language dates remain outside upstream native support; standard relative dates and dynamic inputs are included.

---

## Execution notes

The query and host integration tests cover simple and advanced queries, tasks, priority, relative dates, pending edits, remote changes, and both native UI protocols.
Fixed two upstream tagged-reader defects in [logseq/logseq#13821](https://github.com/logseq/logseq/pull/13821): nested tag forms must be resolved recursively, and tagged constants must be flattening leaves rather than self-recursive nodes.
The vendored source now points directly to that upstream commit; no mobile parser patch or nested-reader workaround remains.
Four native endpoint regressions failed before the fix and passed afterward. The upstream worker `dune runtest` gate and focused seven-test Melange query suite pass.
The full core gate initially found two existing journal-layout checks affected by a new row wrapper.
The renderer now introduces the query wrapper only for Query blocks, preserving ordinary row structure.
The simulator visual check exposed that native buttons do not render arbitrary extension children. Query result entities now use clickable list items, which support rich-text children on both native hosts.

Validation:

- `python3 scripts/vendor-dev-query.py <upstream-checkout> --check` verifies the generated source and license against commit `a63da5cb7993302f8c58ccf42eaf0083b77cf42b`.
- The focused query suite passes all 17 tests, including both native host protocols.
- `dune build @shared/native/runtest` passes all 921 core tests.
- The iOS simulator OCaml core and SwiftUI shell compile successfully; SwiftPM uses `--disable-sandbox` and `-enable-testing`. No XCTest or `swift test` is used.
- `LOGSEQ_RUNTIME_CHECK_ARTIFACTS=/tmp/logseq-query-artifacts python3 apple/Tests/UI/run-simulator-runtime-check.py apple/Tests/UI/query-block-results.swift QueryBlockResultsCheck` passes on the booted iOS 26 simulator. It checks screenshot text recognition, rich result content, folding, navigation, progressive disclosure, empty results, and local errors.
- Visually inspected `/tmp/logseq-query-artifacts/query-block-results.png`; all three rich-text result rows are visible under their Query block.
- `git diff --check` passes. The pre-existing `apple/Package.resolved` edit is preserved.

Android execution and view behavior are covered through the Kotlin host protocol in the core suite; an Android emulator was not run.

---

### Agent Guide 012 Result Views (`docs/agent-guide/012-result-views.md`)

# Result Views Implementation Plan

Goal: Provide native list, table, and gallery views for queries, tag objects, and property objects on iOS and Android.

Architecture: Read saved view blocks and their settings using the master graph schema, and evaluate results against the pending-operation projection.
Reuse the full outliner block renderer and action pipeline for list results.
Keep view state and failures local while sharing native LUI controls between platforms.

Tech Stack: OCaml, DataScript, LUI, SwiftUI, Jetpack Compose.

Related: Builds on `011-query-block-rendering.md`.

## Problem statement

The initial query renderer displays rich text but bypasses normal block selection, editing, children, and breadcrumbs.
Tag and property object collections also need configurable saved views.
References retain their existing presentation.

## Testing Plan

Add regression tests in `shared/test/logseq/query_block_test.ml` for full block rendering on both native host protocols, breadcrumbs, initially collapsed children, expansion, selection, editing, and select bar actions against real RPC sessions.
Include results from other pages and verify their action contexts do not add duplicate rows to the main outline.
Keep ordinary outline collapse state independent from result presentation and prevent recursive query mounting.

Add integration coverage for saved view tabs, list/table/gallery switching, case-insensitive search, master filter operators, multi-key sorting, missing values, and typed reference-backed property values.
Verify tag and property object scopes without changing references.
Exercise malformed saved views beside healthy views and verify errors remain local.
Verify table horizontal scrolling and real property edits through pending operations, including offline projection, invalid input, cancellation, and conflicts.
Run focused tests during development and `rtk proxy dune build @shared/native/runtest` for the final core gate.
Use simulator runtime checks and native platform builds without running `swift test`.
NOTE: I will write *all* tests before I add any implementation behavior.

## Tasks and execution checklist

- [x] Inspect master view storage, filter/sort behavior, native renderers, and the current selection failure.
- [x] Write the complete list interaction regression set and observe behavior failures.
- [x] Decode full result blocks in `shared/src/logseq/response_snapshot.ml` and reuse `View_outliner.outliner_row_body` through a renderer callback in `view_query.ml`.
- [x] Render breadcrumbs and child trees with result-local default folding.
- [x] Resolve result block actions through the RPC action context without polluting the page outline.
- [x] Verify list selection, select bar actions, editing, folding, and ordinary reference behavior.
- [x] Write saved view, search, filtering, sorting, scope, error, and editing integration tests before implementing those behaviors.
- [x] Add a result-view domain module using master `view-for`, `feature-type`, view type, filters, sorting, and column properties.
- [x] Integrate query, tag, and property result views into snapshots and mutation refreshes in `rpc_session.ml`.
- [x] Extend semantic property value codecs only where needed to persist master-compatible settings.
- [x] Add compact native view tabs and search/filter/sort controls, with list, horizontally scrolling table, and responsive gallery layouts.
- [x] Route editable cells through typed pending operations and present local validation or conflict errors.
- [x] Validate both platform protocols, inspect native rendering, update the demo, and record evidence.

## Behavior

| Area | Required behavior |
| --- | --- |
| Scope | Queries, tag objects, property objects; references use their existing renderer. |
| Saved views | Multiple named views stored as master-compatible blocks and selected with tabs. |
| List | Full block renderer, navigable breadcrumbs, children initially collapsed, editing and select bar actions. |
| Search | Per-view case-insensitive search combined with configured filters. |
| Filters | Master operators and typed values, including ref-backed property values and emptiness. |
| Sorting | Ordered sort keys, stable ties, missing values last. |
| Table | Horizontal scroll, readable columns, tappable editable property cells, computed cells read-only. |
| Gallery | Responsive cards with configurable visible properties. |
| Failure | Local error with recovery; siblings and application navigation remain usable. |
| Mobile UI | Accessible controls, comfortable touch targets, native keyboard and dismissal behavior. |

## Testing Details

Core regressions execute actual graph queries and session actions.
UI tests mount the real shared view and dispatch native host events, then inspect rendered blocks and selection controls.
Native runtime checks verify visible text, layout, and interaction on supported devices.

## Implementation Details

- Keep real block UUIDs throughout rendering and actions.
- Inject the full block renderer into result views to avoid a module dependency cycle.
- Keep result expansion independent of graph collapse properties.
- Load action targets from the projected graph rather than only the currently visible page.
- Preserve bounded rendering and avoid recursive nested query evaluation.
- Resolve property references before filtering, sorting, displaying, or editing cells.
- Assign new typed property values to their owner instead of mutating shared value entities.
- Store view settings using existing semantic operation infrastructure.
- Catch invalid configuration and result decoding at each view boundary.
- Preserve unrelated workspace edits.

## Question

No clarification is required to begin.
Multiple saved views follow master's tab interaction.

---

## Execution notes

The list regressions initially failed in five cases: full row rendering, breadcrumbs, folding, selection events, and cross-page actions.
Query result rows now use the same renderer and action pipeline as ordinary blocks on SwiftUI and Kotlin hosts.
Result-local folding starts collapsed and does not mutate source outline collapse state.
Cross-page actions load their source page from the projected database, while snapshots retain the original page scope.
The focused query suite passes 20 tests, the result-view suite passes 17 tests, and the complete core gate passes 941 tests.
The iOS simulator build and `QueryBlockResultsCheck` runtime check pass, including visible rich text and breadcrumbs, child folding, selection toolbar dispatch, and local errors.
Native screenshot evidence is in `/tmp/logseq-result-list-artifacts/query-block-results.png`.
Saved views, search, typed filters, multi-key sorting, table editing, gallery cards, tag collections, and property collections now run through the real core action and snapshot pipeline.
Graph settings remain EDN/DataScript values, including keywords, UUIDs, sets, maps, and vectors.
JSON is the existing native UI transport and snapshot projection, not the graph storage format.

Master conformance uses 1113 cases generated by the production ClojureScript implementation at `05c3ae73e08498e9e20f1b11c3eac5525e5e588d`.
The checked fixture includes physical reference metadata, nullable bounds, empty values, all property kinds, and all three collection scopes.
Membership is verified through the real RPC; sorting is compared using identical candidate order because Datalog result order is unspecified.
The native view algorithm is vendored at `a7191d3d9deba5e34a6fb51d6454ed1d7e680965`.
Upstream filtering and sorting fixes are in https://github.com/logseq/logseq/pull/13823.

The expanded iOS simulator runtime check passes full block rendering, folding, selection, saved tabs, horizontal table layout, cell editing, native settings presentation, gallery, search, and local errors.
Screenshots are in `/tmp/logseq-result-views-artifacts/`.
Native validation exposed an upstream LUI dynamic anchor that emitted an unsupported `stack.display=contents` combination.
Its failing regression and native-compatible box fix are in https://github.com/logseq/lui/pull/255.
The full LUI `dune runtest` gate passes.
The app now pins the corrected OCaml runtime without changing the existing Apple dependency lockfile edit.
Native iOS simulator and signed Release device builds pass.
Android arm64 native core and Debug APK builds pass.

The demo has five simple and five advanced queries, plus 36 saved List/Table/Gallery views across those queries, the Project tag, and the Score property.
The native result-view mutation pipeline submitted all 36 views to the live server and a fresh snapshot at server t=88 preserves them.
A clean import executes all 10 queries with expected row counts and renders all 36 views without configuration errors.
The signed Release app and app-owned demo files have been installed on the paired iPhone.
The existing demo directory is preserved under the app's `Documents/demo-backups/` directory.

CLI batch creation initially lost properties after canonical sync replay because insert payloads omitted subsequent property assignments.
A focused upstream regression reproduces this through actual apply-ops and canonical replay.
The fix retains final property values in inserted block payloads; full upstream native and Melange gates pass.

## Native interaction revision

User review found the first functional UI insufficiently native.
Use native view tabs on the left and separate filter, sort, and search icon actions on the right.
Remove the new-view action from the mobile UI.
Keep the entire collection aligned with the owning block bullet, including nested blocks.
Open a value selector directly from table cells, with immediate selection and explicit creation for open properties.
Use native grouped lists and toolbars for filter and sort configuration, preserving master semantics.
Fix selection toolbar ownership inside pushed block destinations.

- [x] Write interaction, value-creation, layout, and destination-selection regressions and observe failures.
- [x] Implement native tabs, toolbar icons, bullet alignment, and search expansion.
- [x] Implement direct value selection, search, clear, and open-value creation.
- [x] Replace the filter/sort settings screen with native grouped interactions.
- [x] Validate real iOS and Android rendering, then install the revised build on iPhone.


The native revision now has 22 result-view integration tests, including immediate reference selection, explicit page creation, closed-value validation, multi-value creation, filter UUID sets, toolbar ordering, expandable search, and nested bullet alignment.
The full core gate passes 947 tests.
The pushed-block selection regression reproduces the original invisible toolbar on iOS and now verifies visible Copy/Delete/Unselect actions, effect dispatch, and dismissal.
Only the visible navigation destination owns its toolbar; covered destinations and cleared selections do not retain one.
Native iOS screenshots are in `/tmp/logseq-result-views-final-artifacts/` and `/tmp/logseq-selection-artifacts/`.

Android narrow-tab truncation is verified against the actual Compose text layout: the regression fails on the fixed row and passes with Material's scrollable tab row.
LUI light and dark narrow-tab snapshots pass; the app pins `3391c4a56051a0b31eb0b2734ab50337deb5a28b`.
The full LUI Android snapshot gate has six existing baseline failures (four old screenshots and two missing identity video fixtures), reproduced on the original source with byte-identical legacy rendered images.
These existing baselines are preserved.
The mobile app's native interaction checks pass independently.

The filtering/sorting PR https://github.com/logseq/logseq/pull/13823 has been merged into dev.
Reference matching and canonical insert-property retention are submitted separately in https://github.com/logseq/logseq/pull/13825.
The CLI saved-table owner, feature, type, and sorting survive a worker restart; the canonical replay regression also passes in the full upstream gate.
Phone app updates preserve the demo and subsequent user edits, including the extra saved view.
Automatic phone launch is unavailable while the phone is locked; installation succeeds.

Final Android instrumentation passes both complete flows, including actual bullet/toolbar bounds, non-ellipsized selected tabs, reachable saved views through native swipes, a full-width search field, typed cell creation, settings, and error recovery.
Android bullet alignment uses its 24dp slot center (12dp), while the Apple native row also includes its 8pt outer inset (20pt total); both retain 22-unit nested indentation.
The final Android screenshot artifacts are in `/tmp/logseq-result-views-final-android-artifacts/`.
Four new LUI light/dark snapshots for scrollable tabs and search width verify successfully.

## Results toolbar and lazy query revision

The latest interaction replaces the query tab row with one results toolbar: disclosure/count on the left and filter, sort, search, and view-type icons on the right.
Only results and actions share this row; the query owner retains its ordinary block renderer, bullet, content, tags, editing, and selection behavior.
Tag and property collections retain saved view tabs on the left of the same action row.
Saved query views remain accessible through the type menu's Saved views submenu.
List, Table, and Gallery are type menu choices, not synthetic saved tabs.
Collections without saved views show All and default to List; changing the type persists master-compatible view settings.

Queries start collapsed without executing their simple or advanced definitions.
Opening results evaluates the current graph; closing stops query evaluation during later snapshots.
Expanded results include a 24-unit trailing gap before the next block.
Search fits the collection width, and the Apple native field uses one inline background for the icon, input, and clear control.
The app pins LUI `c1a098f2f7893b7714c3a60118413b1c6d24cd50`.

Task priority icons now open a native tap menu on both hosts, using the graph's actual closed values and stable UUIDs.
Selection, cancellation, clearing, row reuse, custom values, persistence, and sync preparation have regression coverage.

The complete core gate passes 951 tests, including the separately submitted graph-link regression.
The query suite passes 21 tests, including normal owner rendering while results are folded; the result-view suite passes 24 tests.
Android instrumentation passes five interaction flows and the URL parser unit tests pass.
Actual iOS runtime checks pass query rendering/search in light and dark themes, the priority popup and effect pipeline, and URL encoding/decoding.
Current screenshots are in `/tmp/logseq-feedback-ios-artifacts/` and `/tmp/logseq-feedback-priority-artifacts/`.

Filter and sort sheets now open at medium height and expand to large, following the calendar utility-sheet interaction.
The native backend resolves theme ancestry through extension parents, reapplies the palette to modal content, and uses the themed background for navigation lists.
The iOS regression reproduced a full-height dark sheet with system gray pixels, and checks the presented height and actual background color after the fix.
The shared settings tests verify both native hosts expose the same expandable sheet contract.
These presentation and theme checks are recorded in the project iOS simulator skill and the reusable iOS development skill.

## Live query type switching regression

The phone's saved type changed correctly, but a table containing missing property values emitted empty buttons without accessible names.
Apple rejected the entire update batch, leaving the previous List renderer visible.
Cells now expose their column and displayed value (or Empty) as their accessible name while preserving an empty visual cell.
The iOS check includes missing values and verifies that switching the current saved view actually replaces the native renderer, then switches back.
Both shared host tests also exercise type changes on a pushed query destination.

A separate bridge regression reproduced loss of earlier operations when one flush produced multiple batches.
The bridge now retains all operations in order and acknowledges their final generation, with a fast path for one batch.
The transport test covers both host profiles and verifies creation precedes the later update.
The complete core gate now passes 954 tests; the actual mobile core and native UI also switch the phone demo's saved query from List to Table to Gallery successfully.

---

### Agent Guide 013 Node Properties (`docs/agent-guide/013-node-properties.md`)

# Node Properties Implementation Plan

Goal: Display and edit page properties immediately and block properties when the block is opened as a node.

Architecture: Project the active node's properties from the real graph using schema metadata and stable UUID references.
Reuse the typed property editor and pending-operation pipeline already used by result tables.
Render text values below subdued labels using the existing block renderer, and render other values beside their labels.

Tech Stack: OCaml, DataScript, LUI, SwiftUI, Jetpack Compose.

Related: Builds on [012-result-views.md](012-result-views.md).

## Problem statement

Pages and zoomed blocks currently lack a complete property surface.
Ordinary outline blocks should remain compact.
The property surface must preserve native editing, cardinality, closed choices, explicit creation, conflict checks, persistence, and sync semantics.

## Testing Plan

Add real-session tests in `shared/test/logseq/node_property_test.ml` before implementation.
Exercise page and block navigation, inherited empty properties, internal metadata exclusion, text reference rendering, typed edits, stale writes, invalid choices, clearing, persistence, and sync transaction preparation.
Exercise both LUI native host profiles, including label styling, block rendering, inline values, native sheets, cancellation, errors, and snapshot refresh.
Add actual iOS and Android runtime presentation checks before implementation.
Run focused tests followed by `rtk proxy opam exec --switch=5.5.0 -- dune build @shared/native/runtest`.
Build mobile targets sequentially because each invokes Dune.
Never run `swift test`.

NOTE: I will write *all* tests before I add any implementation behavior.

## Tasks

1. Add the behavioral tests and observe failures caused by missing property projection and UI.
2. Expose reusable typed property metadata and mutation helpers from `shared/src/logseq/core/result_view.ml` without changing result-view membership checks.
3. Add `shared/src/logseq/core/node_property.ml` and its interface for bounded node-property reads and safe mutations.
4. Project properties in `shared/src/logseq/core/rpc_session.ml` for node routes and selected pages.
5. Add a dedicated node-property effect through `model.ml`, `native_bridge.ml`, and `core_effect.ml`.
6. Decode property projections in `response_snapshot.ml` and retain them in the matching model records.
7. Share the existing property-value sheet in `view_query.ml`, then mount the new `view_property.ml` surface from node and selected-page screens.
8. Verify real native layout in both appearances, typed interaction, and navigation isolation.
9. Run the full core gate, build both platforms, install on the existing iPhone without replacing graph data, and create a reviewable PR.

## Testing Details

Core tests use a persisted graph runtime rather than mocked mutations.
Native host tests send real UI events and inspect emitted core effects.
Runtime checks exercise actual SwiftUI and Compose rendering with empty and multiline values.

The complete native core gate passes 964 tests, including nine new property integration cases.
The cases cover navigation visibility, inherited empty properties, closed values, hidden metadata, typed edits, persistence, sync acknowledgements, stale edits, direct checkbox changes and rollback, multiline editing, existing-value choices, per-value text edits, and built-in page descriptions.
Actual iOS checks pass in light and dark appearances, including leading-edge alignment and a pixel check that property values and checkboxes contain no system-blue accent.
`property-datetime-interaction.swift` plus its Maestro flow exercise actual calendar taps and Done for both date and datetime, verifying the selected day reaches the bridge while retaining the time.
A real mobile-core check against a copied demo graph passes block property edits through `LGCoreEffectExecutor` and immediate page-property rendering.
The copied graph is isolated from the existing phone graph.
Android instrumentation covers the property surface alongside the existing five query-result checks.

## Implementation Details

- Keys use secondary label text without pills or schema identifiers.
- Keys and values use a slightly smaller 16pt font and normal weight; subdued color provides the visual hierarchy.
- Text and URL properties without closed choices occupy their own block rows below the key.
- Short non-text keys share aligned value columns; native text measurement moves long keys above their values.
- Date and datetime values use compact local-time labels and native typed pickers.
- Text property blocks omit breadcrumbs.
- Show the first three properties and reveal the rest with an Expand / Collapse control and direction icon.
- Include properties declared by tags, including inherited declarations, with cycle protection.
- Keep internal and hidden schema properties off the normal surface.
- Preserve existing table editing validation and shared-value isolation.
- Route errors to the property editor rather than the application error screen.
- Preserve the user's existing lockfile edit and phone graph data.
- Page titles have a 16pt gap before properties, the trailing gap is 12pt, and zoomed blocks add no separate leading spacer.

## Question

No blocking product questions remain.
Interpret “all properties” as user-facing assigned and tag-declared properties rather than internal database metadata.

---

---

### Agent Guide 014 Editor Commands (`docs/agent-guide/014-editor-commands.md`)

# Shared Editor Commands Implementation Plan

Goal: Support the agreed master slash commands through the same native completion interaction as page references and tags.

Architecture: The OCaml outliner owns completion state, command discovery, and semantic mutations.
The existing completion panel renders commands, pages, tags, embeds, and templates on both native platforms.
Native date controls and shared input forms handle commands that require additional input.

Tech Stack: OCaml, DataScript, LUI, SwiftUI, Jetpack Compose.

Related: Builds on 013-node-properties.md and the existing outliner reference completion.

## Problem statement

Master supports a graph-aware slash menu, while the native editor currently only completes references and tags.
Support every built-in command except Query, Advanced Query, Query function, Add property, Add comment, Upload an asset, and Node reference.
Do not add custom or plugin commands.
Embed deletion must remove the embedding node and preserve its source and source descendants.

## Testing Plan

Add RPC integration tests in shared/test/logseq/editor_command_test.ml before implementation.
Exercise typing at UTF-16 caret positions, command filtering, every included command family, graph-defined choices, cancel and stale selection, code and escaped literals, template cloning, date and link forms, semantic synchronization, and deleting an embed.
Add shared UI integration coverage for one completion panel, search, dismissal, and choosing a command on both host profiles.
Add native UI regression flows for keyboard retention, page and tag selection, command filtering, input forms, and real calendar taps.
Run focused tests first, then the complete core gate and native platform builds.

Regression tests were added and observed failing before each implementation batch.

## Implementation sequence

1. Add and run the failing integration tests against current RPC and UI entry points.
2. Add shared/src/logseq/core/editor_command.ml and its signature for the master command catalog, graph choices, and command planning.
3. Extend shared/src/logseq/core/outliner_state.ml, outliner_effects.ml, and session_outliner.ml to share selection state and stage atomic semantic operations.
4. Extend rpc_wire.ml, rpc_encode.ml, core_effect.ml, model.ml, native_bridge.ml, and response_snapshot.ml for completion search, submission, and dismissal.
5. Update shared/src/logseq/view_outliner.ml to render one reusable completion panel with compact rows, explicit dismissal, empty states, and common picker navigation.
6. Reuse the existing native-property-date extension for date and datetime inputs and use shared forms for links and media inputs.
7. Render command output with correct headings, list numbering, code language, and linked embed content while retaining wrapper identity for deletion.
8. Run the focused integration suite, the full core gate, iOS simulator checks, Android checks, and phone build and installation without resetting graph data.

## Testing Details

Tests invoke the actual RPC session backed by Graph_runtime and inspect resulting DataScript values and pending synchronization operations.
Embed tests delete a linked wrapper and then read both the source and its child from the real projected database.
UI tests operate the same completion controls for commands, references, and tags, including cancellation without losing the draft.

## Implementation Details

- Preserve master property identifiers and semantic value types.
- Read ordered statuses and priorities from graph definitions.
- Store embeds with block/link and never replace their identity with the source UUID.
- Clone templates into new nodes with fresh UUIDs and preserve source data.
- Respect UTF-16 caret offsets, suffix text, escaped markers, code literals, and input composition.
- Keep command errors inside the editor interaction with a useful recovery path.
- Use the existing app sheet palette and native calendar controls.
- Keep the user's Package.resolved originHash change intact.

## Question

The command scope and exclusions are confirmed by the user.
YouTube timestamp insertion needs the active player position when available and an explicit timestamp input otherwise.

---

## Follow-up behavior

- Pasting a slash, page reference, or tag marker stays literal until a new trigger is typed.
- Code and math editors preserve line breaks; choosing Code in prose creates a separate code block.
- All editable pages expose a lazy trailing placeholder. Opening an empty page does not insert filler; one tap inserts and focuses a real block. Zoomed blocks insert children in their containing page.
- Empty journal pages have independent projection metadata, so an existing day remains visible with no blocks. Active apps refresh the local day after midnight or resume.
- Copied URLs include the graph ID. Graph-less block-ID links resolve in the current graph.

## Validation

- Complete native core gate: 987 tests passed in the combined working tree, including reference-display follow-ups.
- iOS simulator and signed device builds passed.
- iOS native runtime check passed for shared command/reference/tag completion, keyboard and caret retention, and real clipboard paste.
- Android app and instrumentation APKs built; both native completion and clipboard-paste checks passed.
- No graph reset or uninstall is needed for the device update.

## Follow-up parity fixes

- Backspace keeps the native UTF-16 caret at the boundary between merged titles.
- Heading levels precede Clear heading, and graph priorities precede No priority. The duplicate Normal text command is removed in mobile and upstream PR 13826.
- Native disclosure controls expose a 44-point touch target, including its edges.
- Placeholder blocks use the regular bullet asset with reduced opacity.
- The shared properties renderer uses compact task-date icons and local readable datetime labels.

## Editing geometry verification

- Reproduced the plain-text display/editor mismatch with native screenshots: one physical pixel on a single line and a 3.3-point wrapped-height difference.
- Readonly titles now use cached TextKit layout, the editor's measurement, and a pixel-aligned drawing origin. Native checks show zero ink displacement and equal title frames.
- Placeholders are hidden during editing, keep empty journal headings, and share the new empty block's bullet coordinates.
- Android grouped completion buttons retain a fixed height in their vertical scroll container. Native command, page, tag and paste checks pass.
- The full core gate passes 996 cases; the Name cell editor omits Clear value while other properties retain it.

## Compact outliner follow-up

- Reproduced the parent/child gap: the disclosure's 44-point target was participating in row height. It now overlays the first line on iOS; Android uses its compact visual cell with the platform's expanded 48-point touch target.
- The disclosure glyph sits near the trailing edge. A single-line parent and leaf both measure 28.3 points on iOS.
- Task-date properties start at the block text edge, including nesting, task status and priority. The upstream LUI fast-container path ignored individual edge padding; PR https://github.com/logseq/lui/pull/257 fixes the renderer, and the Apple dependency is pinned to that fix.
- Scheduled and Deadline use icons only in compact outliner metadata. Page and zoomed block property panels use ordinary text labels through the same renderer.
- Validation: native iOS disclosure, property alignment and container-padding checks; eight Android spacing/touch-target checks; the complete native core gate passes 996 cases.

- Scheduled and Deadline share one horizontally scrollable compact metadata row. The full page/zoomed property panel retains its regular label layout.
- A native continuation wrapper carries ancestor/child guides through metadata, full property panels and query content. The wrapper uses the same 22-point indentation and 12-point bullet center, draws no hit surface, and suppresses the current child guide when collapsed.
- Native iOS checks verify shared date baselines, guide path coverage and row joins, and both date picker interactions. Eleven Android spacing, property editor and completion checks pass; the full core gate remains green (996 cases).

---

### Agent Guide 015 Native View Filters (`docs/agent-guide/015-native-view-filters.md`)

# Native View Filters Implementation Plan

Goal: Make view filters compact and give every master property/operator combination a suitable native input.

Architecture: Publish the authoritative operator list with property metadata in Result_view.
Keep master filter values and matching in Dev_view, and adapt the shared native sheet around those values.
Reuse the native calendar extension for date and datetime entry.

Tech Stack: OCaml, LUI, SwiftUI, Jetpack Compose.

Related: Builds on 012-result-views.md, 013-node-properties.md, and 014-editor-commands.md.

## Problem statement

The current sheet spends three or more rows on one condition and renders timestamp filters as raw text inputs.
The UI also offers operators that master does not support for several reference types.
A common task such as showing objects created in the last month should require selecting Created and 1 month ago.

## Testing Plan

Add RPC tests for the exact operator lists for every property type and built-in timestamps.
Add shared native UI tests that change property/operator, select relative timestamps, use calendar dates, select checkbox states, search reference values, enter numeric ranges, remove a condition, and save the master-compatible payload.
Verify that a saved relative month value stays relative on subsequent reads and matches real objects around the boundary.
Verify local errors, Cancel, All/Any mode, and multiple conditions preserve unrelated view settings.

NOTE: I will write all tests before I add any implementation behavior.

## Audit

The authority is origin/master at 05c3ae73e08498e9e20f1b11c3eac5525e5e588d.
The operator and value transition rules live in src/main/frontend/worker/handler/view.cljs.
Relative timestamp presets and calendar interaction live in src/main/frontend/components/views.cljs.
The common filter engine lives in deps/db/src/logseq/db/common/view.cljs and its mobile port shared/src/logseq/core/dev_view.ml.

| Property | Master operators | Native input |
| --- | --- | --- |
| Text, URL, node | Is, Is not, Contains, Does not contain | Searchable values for equality; text input for contains |
| Number | Is, Is not, Greater than, Less than, At least, At most, Between | Existing value choices for equality; decimal input and two bounds otherwise |
| Checkbox | Is, Is not | Checked, Unchecked, Empty; one selection |
| Date | Is, Is not, Before, After | Existing day choices for equality; native day picker for comparisons |
| Datetime | Is, Is not, Before, After | Existing values for equality; relative preset or native datetime picker for comparisons |
| Created, Updated | Before, After | Relative preset or native datetime picker |
| Page, entity, class, property and other types | Is, Is not | Searchable existing value choices and Empty |

## Implementation sequence

1. Add failing tests to shared/test/logseq/view_filter_test.ml and native UI checks.
2. Publish filterOperators and timestamp metadata from shared/src/logseq/core/result_view.ml.
3. Replace the repeated Filter N/property/operator rows in shared/src/logseq/view_query.ml with one compact property/operator/remove row and one value row.
4. Add the exact master relative presets and a Created in the last month shortcut that stores after with 1 month ago.
5. Reset incompatible values when changing property or operator using the master transition rules.
6. Reuse temporal_picker for custom dates and retain the selected date with local-time display.
7. Use a searchable selection surface for equality values and clearly distinguish Empty from no selection.
8. Run focused and complete core gates, verify both native sheets, commit independently, and reinstall the phone without resetting graph data.

## Testing Details

Tests drive the actual shared sheet, inspect its submitted effect, execute it against Graph_runtime, and query the resulting view rows.
Boundary tests use Date_time_util to calculate a calendar month rather than assuming thirty days.
Native checks validate visible compact controls and calendar interaction.

## Implementation Details

- Preserve master operator ordering and EDN value shapes.
- Keep relative timestamp strings dynamic; never replace presets with fixed numbers at save time.
- Do not expose Between for timestamps because master only offers Before and After.
- Keep touch targets at least 44 points while removing repeated section headings and redundant controls.
- Keep Cancel and Done in the existing calendar-style themed sheet.
- Keep checkbox values as booleans and reference matches as UUID sets.
- Keep empty-bound numeric ranges supported by the matching engine.
- Hide irrelevant controls as soon as the operator changes.

## Question

The scope and the example workflow are confirmed by the user.

---

## Verification

- The new filter cases first failed for absent operator metadata, missing shortcut and typed controls.
- The complete native core gate passes: 995 tests.
- Filter UI tests run against both shared native host profiles, execute submitted mutations, and verify matching rows and preserved UUID sets.
- The native iOS query-results check covers filter/sort sheet presentation, dark palette, local errors, and cancellation.

---

### Agent Guide 016 Mobile Interaction Regressions (`docs/agent-guide/016-mobile-interaction-regressions.md`)

# Mobile Interaction Regressions Implementation Plan

Goal: Fix the reported mobile editing, navigation, clipboard, date, table, and TestFlight failures with reproducible evidence and native regression checks.

Architecture: Keep graph semantics in the shared OCaml core and reuse existing native SwiftUI/UIKit and Compose controls.
Read extension state from its current owner and preserve native view identity across unrelated updates.

Tech Stack: OCaml, DataScript, LUI, SwiftUI, UIKit, Kotlin, Jetpack Compose.

Related: Builds on `012-result-views.md`, `013-node-properties.md`, `014-editor-commands.md`, and `015-native-view-filters.md`.

## Problem statement

The user reports visible movement on entering block editing, unchanged images loading again, severe jitter during keyboard transitions, and flashes during navigation.
They also report unavailable long-press actions in nested navigation and search, multiple copied blocks pasting as one block, and an unavailable date-format preference.
Table cells need separators and typed editing for every property except Name, Created at, and Updated at.
The specified TestFlight crash must be investigated after these interaction problems are resolved.

## Testing Plan

Run core integration tests through actual RPC dispatch, graph transactions, SQLite pending operations, and confirmed sync replay.
Use `apple/Tests/UI/run-simulator-runtime-check.py` for real iOS control geometry, keyboard transitions, image request counts, navigation controllers, and clipboard behavior.
Use Android instrumentation to exercise equivalent selection, editing, clipboard, and date interactions.
Record the failing behavior before each fix, then run the same test after the fix.
Do not run `swift test`.
Serialize Dune and every mobile build wrapper that invokes Dune.

NOTE: I will write *all* tests before I add any implementation behavior.

## Status and order

| Order | Work | Current evidence and next action |
| --- | --- | --- |
| 1 | Editing position and image retention | Native regression reproduced a 135.67pt shift and repeated image requests; retaining hidden placeholder slots and their height passes the same test. |
| 2 | Keyboard jitter and page flashes | Device diagnostics and native regressions reproduce and verify cached-empty-row height and offscreen editor retention fixes. Repeated native navigation now reads the core path directly; twelve push/pop cycles, nested pop-to-root, root scroll retention and guide presentation bounds pass. The user confirmed the installed idle-jitter fix. |
| 3 | Composer tags | Native controls share property selection; hashtag follows attachment/status. Nine graph-backed/shared-host checks, nine Android composer cases and the complete iOS native select/create/cancel/remove/send/recovery/graph-isolation flow pass. |
| 4 | Trim on leaving editing | Shared RPC tests cover hide, switch, selection, collapse, zoom, Return, autosave, blank content, and failed storage; full core gate passes. |
| 5 | Nested/search long press | The destination-toolbar fix from `bb4575f` passes native direct/nested/search/Recent-root/linked-from-Recent checks through the effect boundary. Actual 800ms touch holds also show the native selection bar on Journal, nested destinations, Recent root, a destination from Recent and a search result; Copy clears it. |
| 6 | Master-compatible clipboard | Thirteen graph-backed round-trip cases cover Markdown/EDN snapshots, collapsed descendants, fresh identities, typed properties/ref remapping, nonadjacent selection, empty/nonempty/parent placement, UTF-16 ranges, paragraphs, alternate list markers, fenced code/math, foreign graphs and atomic failure. The master audit additionally reproduced and fixed nested links when replacing selected text inside an existing formatted link. Native clipboard/selection/rapid-typing/failed-save checks and rebuilt packages pass on both hosts. |
| 7 | Date-format preference | Implemented the master graph property, shared 27-format lexer/formatter, all journal creation paths, live journal/reference/property/navigation labels and graph-scoped native settings menus. Twelve graph/shared-host cases and the full 1,036-test gate pass. Coverage includes storage failure, restart/sync, graph changes, all formats, invalid legacy days, UUID date commands and search-index refresh. Native iOS theme/alignment/confirmed-choice checks and all 19 Android query/editor/property/outliner/settings checks pass. |
| 8 | Table borders and editability | Thirty core cases, the complete iOS native query/view flow and all sixteen Android query/editor/property/outliner checks pass. Name is now editable with protected-name validation; timestamps remain read-only and Tags use the shared typed editor. The Android stretch root cause is fixed upstream with a verified Paparazzi geometry snapshot. |
| 9 | TestFlight crash | The exact report is now correlated through Xcode Organizer's downloaded feedback metadata to crash point `BaQJ15uF9O9nqkuDQ1AILr`, build 2.0.2 (18), revision `ce301717`. Browser login is no longer needed. The main-thread AttributeGraph/LazySubviewPrefetcher failure stack includes native navigation updates, 1.1534 seconds after launch, on iOS 27.0.1. Root cause and a causal fix remain under investigation; the available simulator runs iOS 26.0. |
| 10 | Delivery | The interaction fixes pass the full 1,038-test core gate, native iOS checks and all 28 Android cases. Commit `04c10b4` was installed non-destructively as build `1792698528` and process `23494` was verified alive. PR 69 remains open. Delivery must be repeated after the specified TestFlight crash is resolved. |

## Editing and navigation

1. Keep `apple/Tests/UI/outliner-editor-retention.swift` as a native integration check with multiple journal sections and an uncached image URL protocol.
2. Preserve hidden placeholder layout in `shared/src/logseq/view_rows.ml` and `shared/src/logseq/view_outliner.ml` without exposing a placeholder control while editing.
3. Exercise scrolled long pages, multiline editor handoffs, keyboard dismissal, and guide paths with `apple/Tests/UI/outliner-keyboard-stability.swift`.
4. Exercise real UIKit pushes and pops with `apple/Tests/UI/navigation-content-stability.swift`, including repeated visits to the same UUID.
5. Keep `apple/Sources/Logseq/LGNavigationExtension.swift` coherent by reading depth, child IDs, and bottom presentation state from the live extension model.
6. Verify native search navigation and editor controls with `search-result-navigation.swift`, `search-editor-toolbar.swift`, and `editor-completion.swift`.
7. Audit long-press events through `OutlinerRowExtension.swift`, `shared/src/logseq/core/rpc_session.ml`, and route-specific state projection in `shared/src/logseq/model.ml` before editing that behavior.

## Composer and trim

1. Exercise the existing shared picker in `shared/src/logseq/view_composer.ml` using actual native search, create, Done, Cancel, and chip removal controls.
2. Validate `shared/test/logseq/composer_tag_test.ml` against actual capture, asset upload activation, pending-log restart, and confirmed sync replay.
3. Add recovery tests before adjusting graph changes or failed capture restoration in `shared/src/logseq/model.ml`.
4. Keep `shared/test/logseq/editor_exit_test.ml` as a graph-backed regression suite for edge trimming.
5. Autosave in `shared/src/logseq/core/outliner_state.ml` must preserve draft spaces and caret state while finalization and completed split halves trim their edges.

## Clipboard parity

1. Inspect master in `/tmp/logseq-remove-normal-text/src/main/frontend/handler/editor.cljs` and `handler/paste.cljs`, including custom clipboard payloads and plain-text fallback.
2. Add round-trip integration cases for adjacent roots, descendants, nonadjacent selection, multiline block content, properties, references, and paste at different caret positions.
3. Follow the existing native clipboard bridge in `apple/Sources/Logseq/LogseqApp.swift` and the explicit paste origin in `OutlinerInlineEditor.swift`.
4. Implement parsing and insertion through shared core semantics so Android and iOS share block ordering and hierarchy rules.
5. Verify text copied from other apps still behaves like ordinary text, including text ending in slash, brackets, or a hashtag.

## Dates and tables

1. Inspect master preference persistence and `shared/src/logseq/core/dev_query.ml` formatter support before choosing the mobile representation.
2. Add behavioral tests for changing the date format, restarting the app, existing journal identity, local time, and property/table rendering.
3. Add native table checks for vertical and horizontal separators, horizontal scrolling, sidebar gesture arbitration, and editing every supported property type.
4. Update the shared table and property definitions in `shared/src/logseq/view_query.ml` and `shared/src/logseq/core/result_view.ml` without changing master filter or sort semantics.
5. Reject edits to Name and timestamps at the core boundary as well as in the UI.

## TestFlight crash

Investigate `https://appstoreconnect.apple.com/m/teams/067ef756-c710-4381-8cdd-6ba5c7b89823/apps/1601013908/testflight/crashes/APr-g7HZYfbevY4eiVzs--k` after the preceding work is validated.
Record the affected version, build, exception, crashed thread, symbolication status, and relevant user steps.
Use the actual stack and repository revision to choose the reproduction and fix.
If authentication or crash symbols are unavailable, report that precise missing evidence while continuing any independent repair work.

## Testing Details

The core gate is `rtk proxy opam exec --switch=5.5.0 -- dune build @shared/native/runtest`.
The latest complete gate passes 1,038 tests in 77.285 seconds (`/tmp/logseq-interaction-audit-core-final.log`), including final clipboard link-boundary and additional structure cases, graph date preferences, composer isolation, table policy, cached empty-row renderability and bounded editor retention.
Native image retention checks count actual uncached image requests and compare rendered block coordinates.
Table policy checks pass in `/tmp/logseq-table-policy-core-green.log` and native iOS geometry and interactions pass in `/tmp/logseq-table-divider-native-final.log`.
Composer graph isolation and asset recovery regressions fail before their fixes in `/tmp/logseq-composer-recovery-red.log` and all nine composer cases pass in `/tmp/logseq-composer-recovery-green.log`; three pre-existing asset capture cases also remain green.
Android initially passed 15 of 16 interaction checks, with the new vertical divider geometry case exposing an upstream missing stretch implementation. After fixing stretch and correcting an assertion for a Ready column absent from the Android fixture, all sixteen native checks pass (`/tmp/logseq-stretch-final-android-instrumentation.log`). The upstream geometry snapshot was inspected and passes `verifyPaparazziDebug` (`/tmp/logseq-lui-row-stretch-verify.log`).
Clipboard core regressions fail in `/tmp/logseq-clipboard-red.log` before implementation and pass all eleven graph-backed cases in `/tmp/logseq-clipboard-core-final.log`. Native iOS clipboard transport fails before implementation in `/tmp/logseq-clipboard-native-red-2.log`; native clipboard, exact pre-paste selection, same/different block acknowledgement, rapid typing and failure recovery pass in `/tmp/logseq-clipboard-native-input.log`. The test uses the UITextView delegate input path and the production effect drain, avoiding programmatic `insertText` bypass and parallel test-only drains.
Android clipboard transport fails before implementation in `/tmp/logseq-clipboard-android-red.log`. All eighteen native query/editor/property/outliner cases now pass in `/tmp/logseq-clipboard-android-green.log`, including actual Android clipboard creation/read, same/different block acknowledgements, rapid typing and failed storage recovery.
Direct and nested destination selection, plus a result opened through the native search field, pass `/tmp/logseq-nested-selection-final-2.log`. The check no longer injects selected snapshots manually; long-press effects return the selection projection and Copy/Unselect return the cleared projection.
The expanded check passes `/tmp/logseq-recent-selection.log`: it pops the existing native stack, opens Recent through the actual sidebar control, selects the root, opens a linked destination and verifies selection actions there, then checks search. `apple/Tests/UI/selection-gesture.swift` is its interactive companion. Launch with `python3 apple/Tests/UI/run-simulator-runtime-check.py apple/Tests/UI/selection-gesture.swift --interactive <empty-fixture-directory>` and use actual 800ms touch holds. Computer-use checks verified the visible selection bar and Copy action on Journal, nested pages, Recent root, a destination from Recent and a search destination; the rendered nested/search screenshots were inspected.
The remaining selected-block master audit compares `compose-copied-blocks-contents`, `blocks-for-clipboard`, `paste-copied-blocks-or-text`, `selection-within-link?` and `paste-blocks` with the shared implementation. Root order, ancestor deduplication, copied descendants, snapshot identity, typed values, original reference targets and target placement agree. `/tmp/logseq-clipboard-audit-red-fixed.log` reproduces incorrect nested formatting for a partial selection inside a link. The fix checks whether the UTF-16 selection lies inside a complete formatted link, matching master's replacement rule. All thirteen cases pass `/tmp/logseq-clipboard-audit-green.log`, including a selection between two separate links that must still become a new link. An initial nonadjacent-selection fixture used an invalid fractional order; the corrected valid fixture passes without production changes.
The nested-label case additionally fails `/tmp/logseq-clipboard-nested-link-red.log`. Reusing the project's regex engine with master's non-greedy formatted-link expression fixes bracketed labels without treating text between separate links as linked. All thirteen cases pass `/tmp/logseq-clipboard-audit-final-green.log`, followed by the complete 1,038-test gate. The native clipboard transport and rapid-input/recovery check also passes `/tmp/logseq-clipboard-audit-native.log`.
Final Android rebuild succeeds in `/tmp/logseq-interaction-audit-android-final-build.log`. All 28 native query, completion/clipboard, property, outliner, date-format and composer checks pass `/tmp/logseq-interaction-audit-android-final.log` (234.085s), including actual date preference selection and tag recovery/isolation. Native iOS build and phone packaging succeed in `/tmp/logseq-interaction-audit-simulator-build.log` and `/tmp/logseq-interaction-audit-device-final-build.log`.
Upstream LUI PR 258 now includes Android stretch at `eea10c7bf99ad1a2a40cc84b1c9657a764073d72`; both the Swift package and opam pin reference it. Local Android verification uses `/tmp/logseq-lui-override.gradle` to consume that exact upstream worktree while preserving the user's dirty `duniverse/lui` checkout.
Keyboard checks operate on the scroll view containing the measured block, rather than an arbitrary scroll view in the window.
Navigation checks assert native controller depth so a test cannot pass without a real push or pop.
The repeated-navigation regression reproduces against the date-format head in `/tmp/logseq-navigation-date-head.log`; SwiftUI reports multiple updates to its navigation authority in the same frame. `LGNavigationContent` mirrored core routes into a second `@State` path. That mirror originated in `0e0d6cce`; `7634b389` later animated its synchronization. Removing only that animation still fails `/tmp/logseq-navigation-mirror-no-animation.log`, so disabling animation is not the repair. The native path now reads the core routes directly and retains only an actual native pop pending core acknowledgement. `/tmp/logseq-navigation-final.log` passes twelve real push/pop cycles, nested pushes, pop-to-root, reopening, retained root scroll identity/offset and guide presentation bounds. `/tmp/logseq-navigation-final-system.log` contains no SwiftUI navigation-authority warnings for that check's process.
The native Android composer suite passes all nine cases in `/tmp/logseq-composer-tags-android-green.log`. Its added flows exercise actual tag selection, creation, Cancel, chip removal, unchanged Unicode draft text, typed send payload, failed capture recovery, retry after removing recovered tags, graph changes and stale catalogue responses. The initial final-empty-field assertion incorrectly included the placeholder in Compose semantics; the corrected assertion checks the editable text value without changing production behavior.
The iOS counterpart passes `/tmp/logseq-composer-tags-ios.log`, using actual native multiline text input and the shared native searchable picker. It additionally verifies the composer instance survives tag editing, accepted tagged capture clears its state, and graph switches clear chips. `composer-tag-chip.png` in `/tmp/logseq-composer-tags-ios-artifacts` was visually inspected. The check locates the internal multiline text view rather than expecting SwiftUI's outer accessibility identifier on UIKit, and waits for native typing to enable Send.
The requested larger query-result toggle uses the existing shared chevron icon on both hosts instead of a tiny Unicode triangle. `/tmp/logseq-query-toggle-result-view.log` passes all 30 result-view cases and `/tmp/logseq-query-toggle-query-block.log` passes all 21 query-block cases. `/tmp/logseq-query-toggle-ios.log` passes the full native query interaction check, including fold/unfold, selection, saved views, table cells, filter/sort sheets, gallery, search and contained errors. The actual toggle screenshot was visually inspected.
The navigation repair also passes twelve native page/block search-result pushes and backs in `/tmp/logseq-navigation-search-final.log`, and direct/nested/search selection actions in `/tmp/logseq-navigation-selection-final.log`. Search checks still emit the pre-existing AttributeGraph cycle diagnostics also recorded before this repair in `/tmp/logseq-nested-selection-final-2.log`; these checks pass their behavior and geometry assertions, but those diagnostics remain an audit item.
`/tmp/logseq-navigation-editor-final.log` additionally passes native editor entry/exit across journal sections with stable block coordinates and no extra image request after the navigation change.
Device diagnostic capture `/tmp/logseq-phone-jitter-repro.log` contains 1225 geometry/lifecycle events without note text. The keyboard dismissal at uptime 270565 follows an explicit toolbar press and is not evidence of an unexpected dismissal. Idle geometry oscillations after full snapshots and hydration are confirmed.
`outliner-hydration-stability.swift` reproduces 16pt drift before the cached-empty-row fix; `outliner-scrolling-editor.swift` independently reproduces focus loss when the active block leaves the mounted window.
The cached-empty-row native regression now passes all five refresh cycles. Persistent focused rows also pass forward/reverse scrolling without detachment or a keyboard-hide notification. Native image retention, editor geometry, and multiline keyboard handoff checks remain green. Upstream fixes are in https://github.com/logseq/lui/pull/258 (PR 257 had already merged). The diagnostic fix build was installed and launched at 01:56 on the user iPhone; the user confirmed the flicker/jitter fix at 01:57.
A diagnostic build must explicitly define `LOGSEQ_INTERACTION_DIAGNOSTICS`; Release does not define `DEBUG`. Verify `Library/Caches/interaction-debug.log` can be pulled from the phone before asking for a repro.
Clipboard tests must round-trip actual copied blocks rather than merely validating an intermediate JSON shape.
Final verification includes an iOS build, Android build/instrumentation, and a non-destructive iPhone installation.
At the user's earlier interim installation request, the iOS build (build number `1792693092`) was installed and launched successfully at 02:18 on the paired iPhone, preserving the app container. It adds the tested table policy/dividers and composer graph-isolation/asset-retry fixes to the confirmed jitter fix. Installation/launch evidence is in `/tmp/logseq-table-recovery-install.json` and `/tmp/logseq-table-recovery-launch.json`. Clipboard parity, date-format settings, final native checks and the crash investigation remain open.

## Implementation Details

Date-format implementation notes from the master audit:

- Persist the graph property, not a device-only setting. Scope the mutation by graph ID and reject stale requests.
- Preserve journal UUIDs and stored titles when changing the preference; derive displayed journal titles, reference labels and date-property text from journal day. Native day-picker `editorValue` stays ISO.
- Share the 27 built-in formats already present in `Dev_query.Date_time_util`. `Dev_query` depends on `Graph_read`, so `Graph_read` cannot call back into `Dev_query`: move the pure formatter/token support into the existing pure `Journal` module and reuse it from both.
- Update creation paths in `Graph_runtime.ensure_today_journal`, `Rpc_session.ensure_journal`, `Rpc_ops`, `Pending_pump`, `Editor_command_effects` and date-property creation in `Result_view`.
- Expose the graph preference in snapshots and both native settings menus; preserve graph isolation and avoid changing active date-picker wire values.
- Date-format TDD evidence: `/tmp/logseq-journal-format-red-7.log` reproduces the missing API/menu; `/tmp/logseq-journal-format-surfaces-red.log` reproduces stale open destination titles. Ten cases pass in `/tmp/logseq-journal-format-surfaces-green.log`, and the complete gate passes 1,034 tests in `/tmp/logseq-date-format-full-core-final.log`.
- The added date-command/search cases first fail in `/tmp/logseq-date-command-search-red-fixed.log` and then pass with all twelve date cases in `/tmp/logseq-date-command-search-green.log`. Date commands now insert stable journal UUIDs. Search refresh includes journals and their referring blocks when the graph format changes; the persisted index fingerprint is versioned to rebuild older labels.
- Final date-format verification: `/tmp/logseq-date-picker-complete-core.log` passes all 1,036 core tests. `/tmp/logseq-date-settings-alignment-red.log` reproduces the double-inset/taller custom menu row; reusing the existing native picker contract passes `/tmp/logseq-date-settings-native-final.log`, including light/dark OCR alignment and spacing, colors and confirmed selection. Screenshots in `/tmp/logseq-date-settings-native-final-artifacts` were visually inspected. `/tmp/logseq-date-format-android-all.log` passes all 19 native checks (78.098s), including real dropdown selection and graph isolation.
- Existing journal read tests now assert derived calendar titles. The 10k-row check verifies physical reuse of the derived journal metadata and no stored-title decryption; the sidebar rename-cache check explicitly uses an ordinary page so it retains coverage of actual renames.

- Preserve the user's `apple/Package.resolved` origin hash and pre-existing `duniverse/lui` edits.
- Never reset or uninstall the user's graph data.
- Keep all code and comments in English.
- Keep master clipboard, filter, and sorting semantics authoritative.
- Avoid independent copies of editor, tag picker, and property editor UI.
- Retain standard native controls and consistent theme colors.
- Fix upstream library defects when evidence identifies the library as the owner.
- Update mobile PR 69 and upstream LUI PR 258 with the final scope and verification.
- Do not merge the current PR without a new explicit merge instruction.

## Question

No product clarification blocks the interaction fixes.
The exact TestFlight report was subsequently found in Xcode Organizer's downloaded feedback cache without accessing credentials. Feedback `APr-g7HZYfbevY4eiVzs--k` is explicitly associated with crash point `BaQJ15uF9O9nqkuDQ1AILr`. Its binary UUID `A65B791F-2BEC-3FCE-8616-424B118A4E6C` matches the archived build 18 executable (revision `ce301717`, LUI `e91ee491`). The report describes a background crash notification; the log identifies role NonUI and a main-thread EXC_BAD_ACCESS in AttributeGraph dependency allocation, through LazySubviewPrefetcher and NavigationStackCoordinator, 1.1534 seconds after launch. The device runs iOS 27.0.1. Sanitized correlation evidence is saved in `/tmp/logseq-APr-correlation.json`. The new cold/late app/search restoration check passes on the available iOS 26.0 simulator (`/tmp/logseq-restoration-current.log`), but it has not reproduced this crash or established that the existing navigation repair fixes it. Browser login is no longer required. Do not claim a causal crash fix without further evidence.

---

## Latest interim device install

At the user's next "install first" request, build `1792695320` from mobile commit `89b151a` was built, signed, installed and launched at 02:56 on the paired iPhone. Process `23154` was verified alive. The install preserves the data container and includes the structured multi-block clipboard changes. Date-format work is subsequent and was not included. Evidence: `/tmp/logseq-latest-device-build.log`, `/tmp/logseq-latest-device-install.json`, `/tmp/logseq-latest-device-launch.json` and `/tmp/logseq-latest-device-processes.json`.

At 03:50, build `1792698528` (`LogseqRevision=04c10b4f`) was installed and launched on the same iPhone without uninstalling or resetting its container. Process `23494` was confirmed in the device process list. This build includes date-format settings, the direct core navigation path, larger query chevrons and final clipboard link-selection repair. Evidence: `/tmp/logseq-interaction-audit-device-final-build.log`, `/tmp/logseq-interaction-audit-device-install.json`, `/tmp/logseq-interaction-audit-device-launch.json` and `/tmp/logseq-interaction-audit-device-processes.json`. The specified TestFlight report remains an outstanding requirement; this install does not constitute completion of its crash investigation.


## Query disclosure and hidden date-format follow-up

The user's collapsed query screenshot exposed an unregistered iOS `app:chevron-right` name, which rendered the native missing-icon placeholder. Both query renderers now use the existing `disclosure-right`/`disclosure-down` vocabulary; iOS resolves these to the exact `arrowtriangle.right.fill`/`arrowtriangle.down.fill` symbols used by blocks. Android resolves them to the same arrows as its block disclosure. Shared tests fail before the change (`/tmp/logseq-query-disclosure-core-red.log`) and all 51 query/result-view cases pass afterward (`/tmp/logseq-query-disclosure-core-green.log`). The extended iOS check verifies both actual native icon resolutions, captures both states and passes the complete native interaction flow (`/tmp/logseq-query-disclosure-ios-green.log`). Both screenshots were inspected. All five Android query-view checks also pass (`/tmp/logseq-query-disclosure-android-green.log`).

The user subsequently requested that Date format not appear anywhere in settings. Both native settings entries and their now-unused shared UI builder are removed. Reading the graph's existing format and its underlying data behavior remain supported. Updated shared and iOS native checks first fail on the visible control (`/tmp/logseq-hide-date-format-core-red.log`, `/tmp/logseq-hide-date-format-ios-red.log`). This request supersedes the earlier date-format menu UI requirement and historical menu verification above.


## Object pages and editable table Name follow-up

The user requested that tag/property pages hide their blocks area, including the trailing placeholder. `View_rows.page_entries` now omits only the page outline/placeholder for these two page types and retains object views and references. A shared UI regression covers both native hosts, sidebar roots, linked destinations and search destinations, empty/populated pages and ordinary-page positive controls. It fails before the fix (`/tmp/logseq-object-pages-core-red.log`) and passes afterward (`/tmp/logseq-object-pages-core-green.log`); the iOS native check independently reproduces the old behavior (`/tmp/logseq-object-pages-ios-red.log`).

The user's latest table requirement supersedes the earlier read-only Name rule: Name is editable; Created/Updated timestamps remain read-only. Name uses the existing shared text editor and `Save_title` mutation, keeps its original markup/reference source in the editing field and never offers Clear value. RPC checks cover successful saving, stale-edit rejection and timestamp rejection. The initial metadata regression fails in `/tmp/logseq-name-edit-core-red.log`. Native fixture metadata and interaction checks now follow this updated contract.


## Protected page rename

The requested rename behavior is audited against master `05c3ae73e08498e9e20f1b11c3eac5525e5e588d`, specifically `deps/outliner/src/logseq/outliner/validate.cljs`, `core.cljs`, `deps/common/src/logseq/common/util.cljs` and DB property validation. A common `Node_title` policy protects built-in flags, internal identifiers and file entities, excludes journal renaming, rejects blank page names, hashtags, invalid slashes and page-to-journal conversion, and matches master duplicate scopes and case rules for ordinary pages, tags and properties. `Uunf` is pinned to its Dune Unicode 17 release to match master's NFC lookup keys rather than introducing a partial normalizer.

`Save_title` validates at the pending-operation compile boundary and updates `block/name` for pages. Table Name mutations validate through the same policy; protected rows expose a read-only Name. The page properties projection carries a separate rename capability, allowing ordinary page menus to reuse the existing property editor without exposing a Name row in the properties panel. Rename captures the owner and graph, retains failed edits for retry, closes on graph changes and uses the graph-scoped existing node-property mutation. It never offers Clear value. Existing search invalidation already refreshes a renamed entity and its referring entities; the new graph test verifies this behavior for a warmed index and checks sync compilation.

The new menu, blank-name protection and rename projection fail before implementation in `/tmp/logseq-rename-policy-core-red.log`. The graph-isolation regression independently fails in `/tmp/logseq-rename-scope-red.log`. All twelve node-property cases pass `/tmp/logseq-rename-policy-core-all.log`, including the added native-host menu and graph/search checks. An initial file-protection fixture reused the bootstrap config path and hit its unique constraint; it was corrected to a distinct file path before assessing the policy. No production behavior was changed to accommodate that fixture error.


The final name audit also found that the shared lowercase helper lacked JavaScript's context-sensitive Final_Sigma mapping. `/tmp/logseq-rename-unicode-red.log` reproduces the mismatch, and the expected Greek/apostrophe/Turkish-I cases are verified directly against JavaScript `toLowerCase().normalize("NFC")`. The shared helper now uses Unicode Cased/Case_Ignorable context in linear time. The full core gate passes all 1042 tests in `/tmp/logseq-rename-unicode-core-final.log` (66.242s).

A real RPC follow-up then exposed retained summaries and search pages staying stale even though the index had updated. `/tmp/logseq-rename-retained-red.log` fails on the already selected page's title. Snapshot summaries now resolve the current entity title, and successful title mutations refresh an existing search while retaining its loaded result limit. `/tmp/logseq-rename-retained-green.log` passes selected-page and pushed-route titles, favorites/Recent, old-query membership removal, new-query page/reference matches, stale/graph rejection and sync compilation.

Native iOS rename checks pass `/tmp/logseq-page-rename-ios.log`, covering the shared sheet, failed storage/retry, title replacement, protected-menu removal and graph-change dismissal. The editor/failure screenshots in `/tmp/logseq-page-rename-ios-artifacts` were visually inspected. An initial harness assertion incorrectly expected `runtime.lastError == nil` after an intentionally failed save; it now expects the injected storage error while checking that editing remains available. All nine Android query/object-page, hidden-date-format and actual overflow-menu rename checks pass `/tmp/logseq-page-rename-android-checks.log` (56.563s). The complete native iOS query flow also passes `/tmp/logseq-rename-query-ios-final.log`, including editable Name without Clear value and both resolved disclosure triangles.


The final retained-state fix passes the complete core gate again: `/tmp/logseq-rename-retained-core-final.log`, 1042 tests in 67.575s. The new header/menu also passes cold and late app/search restoration and lazy hydration (`/tmp/logseq-restoration-rename.log`). The final Android app and test APK rebuild succeeds (`/tmp/logseq-rename-final-android-build.log`); the nine targeted native checks above exercise the same final UI behavior. The specified TestFlight crash remains a separate unresolved requirement, with exact report correlation established but no causal crash reproduction or verified fix.


At 04:39, iPhone build `1792701491` (2.0.2, `LogseqRevision=e5c39f73`) was installed over the existing application and launched without resetting or uninstalling its data. Process `23768` was verified alive. This includes the block disclosure icons, hidden Date format controls, object-page blocks/placeholder suppression, editable table Name, protected page rename and retained title/search refresh. Evidence: `/tmp/logseq-page-rename-device-build.log`, `/tmp/logseq-page-rename-device-install.json`, `/tmp/logseq-page-rename-device-launch.json` and `/tmp/logseq-page-rename-device-processes.json`. The final rebuilt Android app/test APKs also pass all nine targeted checks (`/tmp/logseq-rename-final-android-checks.log`, 47.51s). Mobile PR 69 is updated and remains open; the TestFlight crash investigation is not marked complete.

### Isolated TestFlight crash experiments

The simulator runtime runner previously printed captured stdout before captured stderr. That ordering cannot establish whether an AttributeGraph warning happened during navigation or teardown. The runner now merges the streams, and the search regression writes monotonic phase markers to stderr before/after each push, pop and teardown. With that instrumentation, the current implementation passes twelve page/block search-result push/back cycles without cycle warnings (`/tmp/logseq-search-cycle-baseline.log`). This does not establish that earlier cycle diagnostics were harmless or that the TestFlight crash is fixed.

Two controlled source substitutions isolate the build 18 candidates while retaining the current core and the rest of the UI. Both experiments restore the exact original source bytes and rebuild the current simulator in a `finally` block; recorded original/restored SHA-256 values match.

- Exact build 18 `LGNavigationExtension.swift`: search and cold/late restoration checks pass, but the repeated-navigation check fails with native controller counts `[1, 1]`, confirming lost pushes. No AttributeGraph cycle or EXC_BAD_ACCESS is reproduced. Logs and restoration hashes: `/tmp/logseq-navigation-crash-ab-{old-search,old-restore,old-push}.log` and `/tmp/logseq-navigation-crash-ab-state.json`.
- Exact build 18 `LUIVirtualListView`, substituted into the current backend: all three checks pass with no cycle warning or crash. Logs and restoration hashes: `/tmp/logseq-virtual-prefetch-ab-{old-search,old-restore,old-push}.log` and `/tmp/logseq-virtual-prefetch-ab-state.json`. This experiment does not test the complete build 18 combination on iOS 27.0.1.

A separately signed `com.logseq.logseq.RuntimeCheck` app runs mock snapshots in its own iPhone container; it does not replace the installed Logseq app, access its graph container or use its app group. The current iOS 27.2 device run does not complete: the application changes from active to inactive after entering search, and the first native back does not finish the test. The initial run reports failure to reactivate the retained search field. Subsequent diagnostic runs remain incomplete; a SIGKILL during one debugger attempt has no corresponding retrieved RuntimeCheck crash report and is not evidence of the TestFlight SIGSEGV or of a watchdog failure. An LLDB attach observes the main thread in `mach_msg2_trap`, not the reported AttributeGraph allocation stack. Logs: `/tmp/logseq-device-runtime-check-current{,-2,-3}.log`. Foreground availability/test lifecycle must be established before interpreting these native assertions. The exact crash root cause remains unresolved.

The test container's diagnostic log confirms `UIApplicationWillResignActiveNotification` immediately after the search keyboard appears (`/tmp/logseq-device-runtime-check-interaction.log`), without establishing why it resigned. After copying this log, the independently installed RuntimeCheck app was removed (`/tmp/logseq-device-runtime-check-cleanup.json`); the regular Logseq installation/container were not modified. Both experimental source replacements are fully restored, and their final simulator rebuilds succeeded.

Final checks against the restored current simulator pass twelve search-result push/back cycles and all four cold/late app/search restoration cases, with no cycle warning: `/tmp/logseq-crash-audit-current-search.log` and `/tmp/logseq-crash-audit-current-restoration.log`. The runner also passes Python compilation and `git diff --check`. This audit changes test diagnostics/documentation only; the preceding 1042-test core gate and installed application code are unchanged.


### Visible editor ownership and native search surface

The user-device interaction log contains two different `outliner-editor` protocol nodes and two attached UITextViews for the same block. Both repeatedly become first responder, leaving the visible editor without a cursor and restoring another editor's end-of-block position. This is an ownership bug across retained panes, not a reason to force the native caret to the latest core offset.

The historical journal projection only cleared editing while `node_routes` was nonempty. `1c69e277` introduced `navigation_root_model` for retained titles, and `7634b389` applied that projection to the actual iOS root content: it erased the route/search state before the journal projection could check it. In addition, a selected journal page and the retained journal could both match the same editing UUID. Inactive cached node routes could also retain their editor state.

Decide root interaction ownership before clearing navigation state, clear interaction state in the retained journal unless it is the visible journal, and permit only the active visit in each native route stack to project editing/selection/autocomplete. Keep retained rows and scroll identities. No native caret-reset workaround is introduced.

- The new shared integration case fails before the fix with duplicate editors and passes for both iOS and Android through journals, selected journal pages and nested routes. The full gate at that stage passes 1043 tests in 66.493s.
- `outliner-editor-focus.swift` fails before the fix with two editors after selecting a journal page. Afterward it preserves one first responder and native selections at UTF-16 offsets 2, 7 and 0 across stale core acknowledgements. Evidence: `/tmp/logseq-editor-focus-{red,green}.log` and `/tmp/logseq-editor-focus-core-{red,green}.log`.
- `outliner-search-editor-focus.swift` opens an actual search result, taps its block to edit, then follows its real `open-node` row event and repeats editing. The exact build-18 implementation fails with two editors on the first search destination; the current implementation passes both destinations and all caret positions. The nested test uses a row link event, not the external host's app-navigation deep-link action. Evidence: `/tmp/logseq-search-editor-focus-{old,green}.log`.
- Four Android native completion/paste/failure-recovery checks pass against the rebuilt app: `/tmp/logseq-editor-focus-android-checks.log`.

The inline result-search fill used SwiftUI's `.quaternary` foreground hierarchy instead of an app surface token. LUI `df9b11ce` resolves the semantic surface with a system-fill fallback. Native screenshot pixel checks fail on the previous gray fill and pass for light/dark, focused/unfocused and app appearance overriding the system theme. The final pinned consumer query/view workflow also passes (`/tmp/logseq-search-surface-red.log`, `/tmp/logseq-search-surface-green.log`, `/tmp/logseq-query-search-final.log`). Upstream PR 258 is updated. Temporary cached-checkout substitutions were restored before resolving the real new pin.

### Built-in deletion and sidebar icon follow-up

Expose a core `canDelete` capability with node properties, hide Delete in protected page menus on both platforms, and reject a protected or stale confirmation at the shared model boundary. Reuse the core built-in classifier for deletion as well as title editing, including internal class/property identifiers without an explicit built-in flag and hidden system pages. Existing ordinary-page deletion remains available. Focused tests first fail on the missing capability and unprotected internal identifier, then pass with no queued operation or deleted-at mutation for protected pages (`/tmp/logseq-builtin-delete-{red,green}.log`).

Sidebar custom monochrome node icons now use the same subdued native foreground as default page icons. The shared node-icon component carries a presentation-only muted property; page titles and outliner icons retain their normal presentation.

### Full build-18 crash controls

A detached full `ce301717` consumer and its exact Swift LUI pin `e91ee491` build successfully. Its OCaml LUI source is identical to the build-18 opam pin. Unlike the earlier file substitutions, this exercises the complete old core/consumer/backend combination. It passes twelve search push/back cycles, all four cold/late restoration cases and the new native transition-hydration stress check on iOS 26. The latter submits 42 snapshots during animated push/pop transitions; this is not a claim that every intermediate snapshot was individually rendered. Logs: `/tmp/logseq-build18-full-{search,restoration,hydration}.log`. The corresponding device core/Swift library build also succeeds, but was not installed over the user's app.

Archive metadata and the original binary's build-version command confirm SDK 26.0 for build 18, matching this machine's Xcode 26.0 SDK; AppStoreTools 27B47 is not its linked SDK. Across the three retrieved crash reports, AttributeGraph's allocation fault follows an invalid free-list offset. This narrows the failure signature but does not identify who corrupted it. No exact EXC_BAD_ACCESS is reproduced, and the specific iOS 27.0.1 TestFlight crash remains unresolved.

The combined focus, deletion and sidebar changes pass the full core gate: `/tmp/logseq-focus-builtin-core-final.log`, 1045 tests in 66.323s. This includes matching iOS/Android node-icon extension fingerprints, protected delete capability/menu checks and unchanged ordinary-page deletion.

The final rebuilt iOS simulator passes all six native checks: visible editor ownership, search-result/link caret taps, protected page menu/rename failure-retry, sidebar node icons, editor/image position retention and nested/search/Recent selection actions. Logs use `/tmp/logseq-focus-final-<test-name>.log`. The sidebar screenshot was inspected: custom monochrome glyphs match the native document/control secondary gray while content titles retain their normal color.

The final Android app/test APKs rebuild successfully, and the actual protected-page overflow menu plus command/reference/tag completion and paste/failure recovery pass all five instrumentation cases: `/tmp/logseq-focus-builtin-android-final-checks.log`, 34.118s.

At 05:29, iPhone build `1792704538` (2.0.2, `LogseqRevision=63b10d4d`) was installed over the existing app and launched successfully; process `24324` was verified alive. The regular app was not uninstalled and graph data was not reset. This includes the search-destination caret fix, built-in Delete omission, subdued sidebar glyphs and semantic search surface. Evidence: `/tmp/logseq-focus-builtin-device-{build.log,install.json,launch.json,processes.json}`.


### Embed, command-form and view-control follow-up

Embedded nodes now carry an explicit presentation marker from their `block/link` source relation. Outliner rows reuse the existing node-icon renderer for the master `arrow-narrow-right` glyph, including ordered-list embeds; ordinary nodes retain their own icon/number. The existing wrapper-only deletion test remains in the gate. The new core integration first fails without the embed marker and then passes both native row profiles (`/tmp/logseq-embed-arrow-core-red.log`, `/tmp/logseq-form-embed-core-green.log`). The iOS rendered-arrow snapshot and image comparison pass in `/tmp/logseq-native-menu-final/NodeIconsCheck`.

Link/Image link forms compete with the mounted block editor when it unconditionally requests first responder on updates. The shared extension now explicitly transfers focus ownership to picker inputs while keeping the block editor mounted. Native URL and label inputs accept edits across snapshot acknowledgements, submit both values, and return focus to the retained editor on dismissal. Command/reference/tag completion continues to own the block editor. Core focus policy is covered by a failing-then-passing integration check; native iOS form, editor and search/link caret checks pass. Android completion/form/paste/failure-recovery cases also pass (five cases in `/tmp/logseq-menu-forms-android-native.log`; the separate All test in that initial combined run exposed the upstream validation omission described below).

Query result disclosure now uses a separately sized child icon: the existing filled triangle at 10 points on iOS and the platform block chevron at 24 dp on Android. The complete iOS query/view workflow passes, including both disclosure states, cell editing, filters, sort, gallery, search themes and contained errors (`/tmp/logseq-query-menu-final.log`). Its multi-appearance workflow exceeded the old 30-second runtime-check limit under concurrent compilation; the runner now accepts an optional timeout, and the successful run used 60 seconds.

The view selector opts into leading text alignment and retains native adaptive sizing and its touch region. LUI now admits this property in the shared/native schemas and honors it in SwiftUI Menu and Material TextButton labels. Its Android restrictive-property value validator needed the corresponding semantic-value case as well. Protocol RED: `/tmp/lui-menu-alignment-schema-red.log`; GREEN: 20 protocol cases and 9 schema-generator checks. The standalone Android store suite passes 10 cases, including valid start/center/end values and rejection of invalid string/numeric values (`/tmp/lui-menu-alignment-android-unit.log`). The consumer pins LUI `ebfc3273`. iOS screenshot OCR verifies All at the content leading edge while switching ordinary/tag/property pages; Android verifies the actual label edge, adaptive width and opening the selector (`/tmp/logseq-all-leading-android-final.log`, one passing case).

All six iOS native follow-up checks pass: object pages/alignment, link/image forms, the complete query workflow, actual embed glyph, visible editor focus, and search/link caret focus. Evidence is in `/tmp/logseq-native-menu-final` plus the query workflow rerun above. This follow-up does not establish a fix for the exact TestFlight AttributeGraph crash, which remains open.

The final full core gate passes 1047 tests in 77.289 seconds (`/tmp/logseq-menu-forms-core-final.log`).


The expanded Android query/view suite exposed a second upstream validation mismatch: the renderer and shared protocol support button content, but the Android retained tree treated every button as a metadata-only interactive leaf. Exempt buttons/toggle buttons from that leaf-only restriction while retaining it for text fields. A new real-tree regression first fails with the same error as the consumer, then all 12 Android store cases pass (`/tmp/lui-button-labels-android-{red,green}.log`). LUI `4281a516` contains the fix; its Apple and OCaml sources are unchanged from the already validated `ebfc3273` pin.

iPhone build `1792706378` (2.0.2, revision `72ae7c22`, LUI `ebfc3273`) was installed over the existing app and launched; process `24529` was verified alive. It contains the leading view selector, native command-form focus ownership, master embed arrow and block-sized query triangles. The later upstream follow-up changes Android validation/tests only. Evidence: `/tmp/logseq-view-controls-device-{build.log,install.json,launch.json,processes.json}`. Graph data was preserved.

The final rebuilt Android consumer passes all eight query/view instrumentation cases in 33.96 seconds (`/tmp/logseq-query-controls-android-final.log`), including the collapsed-query button label, selector bounds/menu, priority popup, saved views, object pages, table edits, filter/sort sheets, gallery, search and error containment. The upstream store suite passes all 12 cases.


### Single-window lifecycle controls for the exact crash audit

The preceding goal turn made product/test/installation progress. This follow-up replaces the earlier physical probe’s SwiftUI placeholder window plus second UIWindow with one app-owned UIWindow in a UIKit scene delegate. The test task belongs to the scene delegate, so replacing the hosting root does not cancel a SwiftUI `.task`. Every phase records application/scene activation state and monotonic time in the independent probe container. The binary links the complete prebuilt build-18 consumer/core/backend (`ce301717`, original LUI pin); snapshots and effects are synthetic. This is an isolated native runtime control, not the original SwiftUI App shell or a real authentication/network/storage bootstrap.

The source/build manifests and controls are archived at `/tmp/logseq-lifecycle-probe-sources.zip` (21,677 bytes; SHA-256 `9b66a36820813ccf43278c05379bdf0cf6a1f18ef71ffa63f8bac11190307047`). Both manifests match the current four Swift source hashes. The prototype source/build helpers are in `/tmp/logseq-lifecycle-probe`. Builds use existing native objects and never invoke XCTest or modify the regular app’s graph container. Simulator launches used `simctl --checked-allocations`; this records the requested launch option, not proof that every allocator has an active diagnostic hook.

On iOS 26, all four cold/late app/search restoration cases pass in the new lifecycle shell. The native animated hydration control also completes. Forcing that animation assertion while the scene is backgrounded instead fails its prerequisite (an animated push must start); this is not the reported EXC_BAD_ACCESS. The dedicated background control therefore validates real phase transitions, submits data/session updates while backgrounded, then verifies the final native UILabel content after foreground restoration. It runs three cases: already-loaded rows; no initial graph projection/graph-picker state; and the production-like initial `graph-loading=true` navigation shell before graph data arrives. Each submits ten row/depth changes and completes foreground recovery. Warm and loading-shell logs show actual native navigation depths 1/2/3 during background updates; the graph-picker case defers native navigation creation until foreground. These observations must not be conflated. Logs: `/tmp/logseq-lifecycle-background-{warm-control,cold-loading}.log`, `/tmp/logseq-lifecycle-background-cold-control.log`, `/tmp/logseq-lifecycle-simulator-restoration.log`, and merged lifecycle/diagnostic events in `/tmp/logseq-lifecycle-probe-events.log`. The final loading-shell rerun captures merged stdout/stderr and writes monotonic phase markers directly to stderr (`/tmp/logseq-lifecycle-probe/background-simulator-console.log`, `/tmp/logseq-lifecycle-background-console-control.log`). It completes in 8.7143 seconds without the allocation fault or a cycle warning in that console capture. The earlier phase/unified-log-only files cannot establish stderr-warning absence. These simulator cases launch in the foreground and are then backgrounded by the host after readiness; they are not no-activate background-only launches.

The first real-device background launch was rejected by SpringBoard with `FBSOpenApplicationErrorDomain` code 7: the device was locked and could not be unlocked (`/tmp/logseq-lifecycle-background-launch.json`). A subsequent launch and physical background/resume control completed; see the follow-up below. The independently signed probe uses `com.logseq.logseq.LifecycleProbe`, its own container and no app-group entitlement. The regular installed build remains `1792706378`, and its process `24529` was still present after the successful physical control.

Device inventory also limits comparison: the connected phone is `iPhone15,4` (iPhone 15), iOS 27.2 build `24B5110a`; the exact report is `iPhone18,1`, iOS 27.0.1. Other listed devices are unavailable. Even after unlocking, a pass on the connected phone cannot by itself prove the original hardware/OS crash is fixed. The exact crash root cause and causal repair remain outstanding. The crash role/comment provide context, not a complete UIKit scene-activation timeline.

### Physical recovery and mixed-content crash controls

The same Organizer crash point contains three unique incidents with the same `AG::data::zone::alloc_bytes_recycle → LazySubviewPrefetcher → prepareNavigationBar → NavigationStackCoordinator` stack. All report iPhone18,1 / iOS 27.0.1. Build 18 is Non UI at 1.1534 seconds; the two build-17 reports are Foreground at 28.7823 and 16560.9842 seconds. Background cold launch therefore does not explain the entire signature. This establishes neither one reporter nor distinct reporters. Sanitized audits: `/tmp/logseq-attributegraph-{signature-audit,report-environments}.json`.

The exact feedback was also located in Xcode's cached App Store Connect response. Its supplementary metadata contains no page, graph, navigation trace or additional reproduction steps; the app-uptime field is null. Raw tester metadata and request headers were not copied to the evidence archive. A question requesting reproduction steps or a matching environment remains pending. Xcode's official `-downloadPlatform iOS -buildVersion 27.0.1 -architectureVariant arm64` lookup exits 70 because that runtime is unavailable for download. The only installed simulator runtime is iOS 26.0.

The independent probe subsequently ran on the connected phone. A no-activate launch succeeded, but its phase log records an active scene before backgrounding: `activatedWhenStarted=false` does not establish a background-only execution. Its search-restoration animation prerequisite failed while backgrounded; this is not the reported allocation fault (`/tmp/logseq-lifecycle-device-background-cold-final.log`).

The dedicated physical warm control completed ten row/navigation updates while both app and scene were backgrounded, with observed native navigation depths 1/2/3. Reactivating the same process verified revision 10 in native content, then recorded `CHECK_COMPLETE` at 35.0722 seconds (`/tmp/logseq-lifecycle-device-warm-final.log`). The cause of the initial background transition was not recorded. A transient CoreDevice log-copy socket failure was retried without restarting the process. The regular Logseq app was brought back to the foreground afterward, and process 24529 was verified alive. This validates background-update recovery on iPhone15,4 / iOS 27.2, not a repair of the original crash.

The old build was also exercised with stable row identities changing between bold text, quotes, formulas, highlighted code, local images and node references during six real native pushes and six pops. Screenshot inspection caught an invalid image-type fixture and then an unfinished image decode; the final fixture uses `png` and waits for the rendered orange image before starting navigation. The saved screenshot confirms actual image, formula, code and quote content. All 42 content submissions occur while a native transition coordinator exists. The final merged stdout/stderr run completes at 8.9337 seconds without the matching fault or a cycle warning (`/tmp/logseq-lifecycle-mixed-verified-image-console.log`). A physical mixed-content attempt was rejected before launch because the phone was locked; no mixed-content device pass is claimed.

The follow-up simulator sources, verified source-hash manifest, screenshot and sanitized controls are archived at `/tmp/logseq-lifecycle-mixed-followup-sources.zip` (266,883 bytes; SHA-256 `e91119bb9c7f411624a3b738b5caf25934e83fd1d603aa778a629909ced72bfe`). This is separate from the earlier lifecycle archive; its current simulator manifest must not be used to identify the earlier physical binary. No production code or app installation changed in this crash-audit follow-up. The exact crash has not been reproduced and its causal repair remains open.

### Invisible collapsed query disclosure regression

The user's S1 screenshot was reproduced with the original demo graph's mobile-session response: the query and its 44-point Results button exist, but its native label and icon are absent. Commit `72ae7c2` changed the disclosure to button content children for an independently sized icon. The Apple renderer still rendered only the button's text/icon properties and ignored content children, leaving the default collapsed query's sole entry point blank. Earlier native checks verified the retained icon model and expanded results without asserting that the initial Results label was actually drawn.

LUI `71964a68` renders Button/ToggleButton label children through the existing retained-node renderer, excludes context-menu metadata, and keeps the normal text/icon fallback. Its native runtime check fails on the old renderer with `Fallback Plain`, then passes custom labels, child text updates, metadata exclusion and fallback restoration (`/tmp/lui-button-children-native-{red,green}.log`). The consumer pins the same revision in SwiftPM and opam.

The new `apple/Tests/UI/query-disclosure-lifecycle.swift` first fails because Results is absent from the screenshot. After the fix, it passes actual rendered Results text and 10-point glyph frames, lazy expansion, updated counts, folding and re-execution on ordinary pages and zoomed query blocks (`/tmp/logseq-query-disclosure-lifecycle-{red,green}.log`). A separate real-session core round trip covers simple/advanced queries on both hosts, including zoom-in; it confirms that execution itself was intact. The full core gate passes 1048 tests in 66.724 seconds (`/tmp/logseq-query-disclosure-core-final.log`). The rebuilt complete iOS query workflow also passes lists/selection, saved views, horizontal tables, cell editing, filter/sort sheets, gallery, search and local errors (`/tmp/logseq-query-fixed-workflow.log`). This fixes the missing query entry point; it does not establish a repair of the unrelated TestFlight crash.

### Virtual table and gallery results

Table and gallery views previously retained every row exposed by successive Show more presses. They now reuse the outliner's bounded window protocol: long collections have a 480-point native lazy viewport, with at most 32 mounted rows. A wide gallery groups two cards per virtual row and preserves the final unpaired card. Short collections retain their natural height. Tables keep a fixed header, horizontal scrolling, column dividers and the shared typed cell editor. Search and changed view configurations reset the result viewport; an editor's state lives outside the recyclable rows. Query results, tag objects and property objects share this collection implementation.

The new shared regressions first fail because no virtual window exists, then pass 10,001-row traversal, node eviction, distant cell editing, search/empty results, sorting, and gallery resizing on both native hosts (`/tmp/logseq-virtual-results-{red,green}.log`). The final core gate passes 1,050 tests in 98.269 seconds (`/tmp/logseq-virtual-results-core-final.log`). The real iOS check in `apple/Tests/UI/virtual-result-views.swift` scrolls native UIScrollViews to the final row and back, verifies rendered text with OCR, and opens the last table row's editor. Screenshots and logs: `/tmp/logseq-virtual-results-final-artifacts`, `/tmp/logseq-virtual-results-native-final.log`. The existing complete query workflow also passes (`/tmp/logseq-virtual-results-final-existing-flow.log`).

Refresh regressions also fail before the layout fix on the core and both native platforms: a value update resets the core window while the native scroller remains at the bottom, leaving the current rows unloaded. The collection now compares only layout settings, using semantic JSON equality so object-key order does not reset the window. Row subscriptions belong to their recyclable scopes. Core and native checks verify that distant values update without losing the viewport. All 10 Android query/view instrumentation cases pass, including 10,001-row table/gallery traversal, distant edits, value refreshes, and the existing horizontal scrolling, selectors and settings checks (`/tmp/logseq-virtual-results-final-android-suite.log`).

That native check exposed a LUI scroll-back bug: `onDisappear` unregisters a virtual row, but SwiftUI can reuse its last visible geometry without a new geometry callback. LUI `db5cca50` restores the cached visible registration on appearance. The consumer reproduces the missing range callback before the fix and passes afterward. A standalone LUI runtime check also passes repeated distant scrolls and returns (`/tmp/lui-virtual-list-reentry-native-green.log`). Upstream: https://github.com/logseq/lui/pull/260. No XCTest was used.

---

### Agent Guide Android Ios Release Qa 2026 10 21 Android Performance.Json (`docs/agent-guide/android-ios-release-qa/2026-10-21/android-performance.json`)

{
  "device": "emulator-5554",
  "variant": "profile (debuggable)",
  "launches": [
    {
      "first_ui_rendered": 1124.0,
      "journals_ui_ready": 1428.0,
      "authentication_published": 1479.0,
      "am_total_ms": 2089
    },
    {
      "first_ui_rendered": 1305.0,
      "authentication_published": 1459.0,
      "journals_ui_ready": 1758.0,
      "am_total_ms": 1513
    },
    {
      "first_ui_rendered": 1962.0,
      "authentication_published": 2366.0,
      "journals_ui_ready": 2707.0,
      "am_total_ms": 2237
    },
    {
      "first_ui_rendered": 1877.0,
      "journals_ui_ready": 2541.0,
      "authentication_published": 2592.0,
      "am_total_ms": 2539
    },
    {
      "first_ui_rendered": 4150.0,
      "authentication_published": 5041.0,
      "journals_ui_ready": 5865.0,
      "am_total_ms": 4538
    }
  ],
  "idle_10s": {
    "frames": 6,
    "janky_frames": 6,
    "p50_ms": 150,
    "p95_ms": 250,
    "p99_ms": 250,
    "slow_ui_thread": 3,
    "jank_percent": 100.0
  },
  "scroll_1": {
    "frames": 235,
    "janky_frames": 59,
    "p50_ms": 28,
    "p95_ms": 65,
    "p99_ms": 97,
    "slow_ui_thread": 42,
    "jank_percent": 25.11
  },
  "scroll_2": {
    "frames": 188,
    "janky_frames": 51,
    "p50_ms": 28,
    "p95_ms": 129,
    "p99_ms": 350,
    "slow_ui_thread": 41,
    "jank_percent": 27.13
  },
  "scroll_3": {
    "frames": 234,
    "janky_frames": 44,
    "p50_ms": 26,
    "p95_ms": 65,
    "p99_ms": 101,
    "slow_ui_thread": 33,
    "jank_percent": 18.8
  }
}

---

### Agent Guide Android M3 Quality 2026 10 22 Expressive Apk.Json (`docs/agent-guide/android-m3-quality/2026-10-22/expressive-apk.json`)

{
  "apk_sha256": "044d6d66fb27f2b55525938460da19738a77dd975c7a12a2e557fef65350e4a1",
  "core_sha256": "a8f42c68cecc15048297e2eae35689a0e59033888199992d699c8897ca87e217",
  "build_revision": "60a74052",
  "variant": "release",
  "material3": "1.5.0-beta01",
  "compose_bom": "2026.09.00",
  "changes": "Expressive menus and single-selection items, native tabs, full-row checkbox/switch/radio interaction; includes the previous composer/defaults batch",
  "validation": {
    "instrumentation_passed": 19,
    "jvm_passed": 57,
    "release_build": "BUILD SUCCESSFUL",
    "apk_signature": "verified",
    "tested_debug_core_matches": true
  },
  "physical_device_install": {
    "device": "Redmi",
    "mode": "adb install --no-incremental -r",
    "result": "Success",
    "data_cleared": false,
    "launch_status": "ok",
    "launch_state": "COLD",
    "am_total_time_ms": 450,
    "note": "One launch receipt, not a controlled performance benchmark"
  },
  "status": "Full component migration and controlled performance validation remain in progress"
}

---

### Agent Guide Android M3 Quality 2026 10 22 Native Surfaces Apk.Json (`docs/agent-guide/android-m3-quality/2026-10-22/native-surfaces-apk.json`)

{
  "apk_sha256": "4b753185f54a4c28dc6d492856ad8e6b8124892ac1ff4f5109e1fd60455cc564",
  "core_sha256": "a8f42c68cecc15048297e2eae35689a0e59033888199992d699c8897ca87e217",
  "build_revision": "1c34ed0e",
  "variant": "release",
  "material3": "1.5.0-beta01",
  "changes": "Native modal drawer and popover; expressive button shapes and icon variants; native list selection; Material typography and surface token defaults",
  "validation": {
    "focused_instrumentation_passed": 35,
    "retained_content_regressions_passed": 17,
    "jvm_passed": 57,
    "signature_verified": true,
    "tested_debug_core_matches": true
  },
  "physical_device_install": {
    "device": "Redmi",
    "mode": "adb install --no-incremental -r",
    "result": "Success",
    "data_cleared": false,
    "launch_status": "ok",
    "launch_state": "COLD",
    "am_total_time_ms": 497,
    "note": "One launch receipt, not a performance benchmark"
  },
  "physical_appearance_capture": {
    "driver": "Native accessibility actions; no system input injection or content edits",
    "passed": true,
    "seconds": 10.952,
    "screens": ["main", "drawer", "attachments", "status"],
    "screenshots": "Private local files under /tmp/logseq-redmi-m3-*.png"
  },
  "emulator_measurement": {
    "janky_frames": 21,
    "frames": 974,
    "jank_percent": 2.16,
    "note": "No meaningful improvement established; controlled optimization and tracing remain required"
  },
  "remaining": "Android Material composer, other component API/default migrations, controlled performance profiling and optimization, upstream publication and final validation"
}

---

### Agent Guide Android M3 Quality 2026 10 22 Preview Apk.Json (`docs/agent-guide/android-m3-quality/2026-10-22/preview-apk.json`)

{
  "apk_sha256": "7d7c652bf32132d5233906dadba93cf704a9b37b67e09f1f1bf694bf69f194cd",
  "core_sha256": "a8f42c68cecc15048297e2eae35689a0e59033888199992d699c8897ca87e217",
  "build_revision": "60a74052",
  "variant": "release",
  "material3": "1.5.0-beta01",
  "compose_bom": "2026.09.00",
  "status": "First validated batch; full expressive component migration remains in progress",
  "signing": "Local test key, preserves current installation signature",
  "physical_device_install": {
    "device": "Redmi",
    "mode": "adb install --no-incremental -r",
    "result": "Success",
    "data_cleared": false,
    "launch_status": "ok",
    "launch_state": "COLD",
    "am_total_time_ms": 454,
    "note": "One launch receipt, not a controlled performance benchmark"
  }
}

---

### Agent Guide Android Release Quality 2026 10 21 Apks.Json (`docs/agent-guide/android-release-quality/2026-10-21/apks.json`)

{
  "before": {
    "apk_sha256": "7f191995dc5274106763af1a3256c26e0eaa27aad6e42884e5abe1d09f317da2",
    "bytes": 23040191,
    "native_sha256": {
      "lib/arm64-v8a/libandroidx.graphics.path.so": "41e9a793c43a0f4fddb19e33f346bace464f30f888ba7b9eaf96294ea115bfb6",
      "lib/arm64-v8a/liblogseq_core.so": "75b5b4426e92664f238c0412383f9d43cb4ced28d8a6e62445fde4c434a8eda5",
      "lib/arm64-v8a/liblogseq_jni.so": "dc561c30a0898dfe8dfe4127528679729ee3a6beb69c5096bd7b41c5c59fd7c7"
    }
  },
  "after-no-profile": {
    "apk_sha256": "d3578c6116e5489dfab5c652fab4817b5624ce66d83e33fa073cb144897c5e65",
    "bytes": 23585803,
    "native_sha256": {
      "lib/arm64-v8a/libandroidx.graphics.path.so": "41e9a793c43a0f4fddb19e33f346bace464f30f888ba7b9eaf96294ea115bfb6",
      "lib/arm64-v8a/liblogseq_core.so": "75b5b4426e92664f238c0412383f9d43cb4ced28d8a6e62445fde4c434a8eda5",
      "lib/arm64-v8a/liblogseq_jni.so": "dc561c30a0898dfe8dfe4127528679729ee3a6beb69c5096bd7b41c5c59fd7c7"
    }
  },
  "after-profile": {
    "apk_sha256": "cb1f50c853e0a77937e666246c06fe6c2a1a977d87c69e66ebcd3c0b53874da1",
    "bytes": 23589899,
    "profile_bytes": 14005,
    "profile_rules": 2740,
    "native_sha256": {
      "lib/arm64-v8a/libandroidx.graphics.path.so": "41e9a793c43a0f4fddb19e33f346bace464f30f888ba7b9eaf96294ea115bfb6",
      "lib/arm64-v8a/liblogseq_core.so": "75b5b4426e92664f238c0412383f9d43cb4ced28d8a6e62445fde4c434a8eda5",
      "lib/arm64-v8a/liblogseq_jni.so": "dc561c30a0898dfe8dfe4127528679729ee3a6beb69c5096bd7b41c5c59fd7c7"
    }
  },
  "api33-overlay-profile": {
    "apk_sha256": "8e14660714e37aabfe50f556301e08a2720077a222377d2f57efb28cec3b7cd6",
    "bytes": 23598091,
    "native_sha256": {
      "lib/arm64-v8a/libandroidx.graphics.path.so": "41e9a793c43a0f4fddb19e33f346bace464f30f888ba7b9eaf96294ea115bfb6",
      "lib/arm64-v8a/liblogseq_core.so": "75b5b4426e92664f238c0412383f9d43cb4ced28d8a6e62445fde4c434a8eda5",
      "lib/arm64-v8a/liblogseq_jni.so": "dc561c30a0898dfe8dfe4127528679729ee3a6beb69c5096bd7b41c5c59fd7c7"
    },
    "app_baseline_profile": true
  },
  "api33-final": {
    "apk_sha256": "dce70138ab748c55c5d2dc94a583f29626e99a7ce1a4bc48093e4cd8349b7a52",
    "bytes": 23593995,
    "native_sha256": {
      "lib/arm64-v8a/libandroidx.graphics.path.so": "41e9a793c43a0f4fddb19e33f346bace464f30f888ba7b9eaf96294ea115bfb6",
      "lib/arm64-v8a/liblogseq_core.so": "75b5b4426e92664f238c0412383f9d43cb4ced28d8a6e62445fde4c434a8eda5",
      "lib/arm64-v8a/liblogseq_jni.so": "dc561c30a0898dfe8dfe4127528679729ee3a6beb69c5096bd7b41c5c59fd7c7"
    },
    "app_baseline_profile": false
  },
  "integration-final": {
    "apk_sha256": "598181f75e3d60e843a38700e9d52998b17540821d3cdd7b369f16819fe7e19f",
    "bytes": 23602187,
    "native_sha256": {
      "lib/arm64-v8a/libandroidx.graphics.path.so": "41e9a793c43a0f4fddb19e33f346bace464f30f888ba7b9eaf96294ea115bfb6",
      "lib/arm64-v8a/liblogseq_core.so": "403d54681802b7ed41ebdeaf73c6e7b428528e1c655bbe3499b10387b3511ce8",
      "lib/arm64-v8a/liblogseq_jni.so": "dc561c30a0898dfe8dfe4127528679729ee3a6beb69c5096bd7b41c5c59fd7c7"
    },
    "root_revision": "a616c08f60101e95b55e8a8da36abfd8a53eeaa3",
    "lui_revision": "d01cb95cb0de3bdd2710093ce9b0ed739cc00a18",
    "app_baseline_profile": false
  },
  "simplified-integration": {
    "apk_sha256": "41866ac69ea0437b203963d936c7cd98611ffee3986835d565137fc436ab65ef",
    "bytes": 23593995,
    "native_sha256": {
      "lib/arm64-v8a/libandroidx.graphics.path.so": "41e9a793c43a0f4fddb19e33f346bace464f30f888ba7b9eaf96294ea115bfb6",
      "lib/arm64-v8a/liblogseq_core.so": "51332dec49f00dcf822f44cbc2536ad25723c45f03c9f8f1f8819c86ff539932",
      "lib/arm64-v8a/liblogseq_jni.so": "dc561c30a0898dfe8dfe4127528679729ee3a6beb69c5096bd7b41c5c59fd7c7"
    },
    "lui_revision": "cd7c7b44885e61a28510ca0454a409a82f59dfee",
    "app_baseline_profile": false
  },
  "final-integration": {
    "apk_sha256": "c18e81ae40cce880d5b268531118ea3e1153686967b4e5629dbd9e9c6a6d9319",
    "bytes": 23593995,
    "native_sha256": {
      "lib/arm64-v8a/libandroidx.graphics.path.so": "41e9a793c43a0f4fddb19e33f346bace464f30f888ba7b9eaf96294ea115bfb6",
      "lib/arm64-v8a/liblogseq_core.so": "36bb0c6d69fbcbb0ed4a90388db284a2c62e5020d9335d918e78982c86e5a0a8",
      "lib/arm64-v8a/liblogseq_jni.so": "dc561c30a0898dfe8dfe4127528679729ee3a6beb69c5096bd7b41c5c59fd7c7"
    },
    "app_base": "2ad7d02e3374fee66f6728bb7c2a57e140bb9a04",
    "lui_revision": "1edf12691bcd78ee65b52355d5c2a35f4194524c",
    "app_baseline_profile": false,
    "status": "Historical intermediate package; superseded by final-frozen-release. Not the final measurement artifact."
  },
  "final-frozen-release": {
    "apk_sha256": "4d657a380c2a80d6066beb81075c2b8c3433b12f98b6e521c447b9a1310a5290",
    "native_sha256": {
      "lib/arm64-v8a/libandroidx.graphics.path.so": "41e9a793c43a0f4fddb19e33f346bace464f30f888ba7b9eaf96294ea115bfb6",
      "lib/arm64-v8a/liblogseq_core.so": "d296f89ec569e49b2c87454cbbb21c766a3d4751efba06f7cb923b5920b021b9",
      "lib/arm64-v8a/liblogseq_jni.so": "dc561c30a0898dfe8dfe4127528679729ee3a6beb69c5096bd7b41c5c59fd7c7"
    },
    "build_revision": "2ad7d02e",
    "lui_revision": "1edf12691bcd78ee65b52355d5c2a35f4194524c",
    "bytes": 23593995,
    "app_baseline_profile": false,
    "signing": "Local test key, not production"
  }
}

---

### Agent Guide Android Release Quality 2026 10 21 Completion Audit.Json (`docs/agent-guide/android-release-quality/2026-10-21/completion-audit.json`)

{
  "goal_scope": "Android renderer fixes, trace-driven patch optimization, emulator comparison and updated report/demo",
  "lui_main": "e33a152120dff61a65217130d9479f0e974fde47",
  "lui_previous_main": "03cc08063adec1c2c0d7a921beca192e6a63153e",
  "lui_tested_pin": "1edf12691bcd78ee65b52355d5c2a35f4194524c",
  "lui_main_and_tested_pin_have_identical_source_trees": true,
  "lui_push": "Normal fast-forward push, remote refs/heads/main verified",
  "jvm": {
    "tests": 54,
    "failures": 0,
    "errors": 0,
    "skipped": 0
  },
  "instrumentation": {
    "tests": 17,
    "log": "patch-pipeline/final-frozen-ui.log"
  },
  "final_build": "patch-pipeline/final-build-gate.log",
  "final_release": "final-integration-performance/apk.json",
  "final_performance": {
    "janky_frames": 27,
    "frames": 993,
    "all_three_emulator_budgets_pass": true
  },
  "final_demo": {
    "path": "android-final-demo.mp4",
    "sha256": "0c7cd6c4f2d591235396595b5735d7379d8a5f5d51f2d3c7628f99d5a70e273e",
    "duration_seconds": 24.829056,
    "native_screenrecord": true
  },
  "ios_modified_by_this_task": false,
  "rejected_experiments_shipped": false,
  "remaining_formal_release_validation": [
    "Redmi actual-device performance",
    "Real authenticated editing/persistence/sync/attachments",
    "Chinese IME",
    "Production signing/distribution"
  ],
  "workspace_note": "Other concurrent work is preserved. Frozen APKs, not later workspace HEAD, identify the validated integration snapshot."
}

---

### Agent Guide Android Release Quality 2026 10 21 Demo Attempt Demo Metadata.Json (`docs/agent-guide/android-release-quality/2026-10-21/demo-attempt-demo-metadata.json`)

{
    "programs": [

    ],
    "stream_groups": [

    ],
    "streams": [
        {
            "codec_name": "h264",
            "width": 1080,
            "height": 2400
        },
        {

        },
        {

        }
    ],
    "format": {
        "duration": "34.069878",
        "size": "6526646"
    }
}

---

### Agent Guide Android Release Quality 2026 10 21 Demo Metadata.Json (`docs/agent-guide/android-release-quality/2026-10-21/demo-metadata.json`)

{
    "programs": [

    ],
    "stream_groups": [

    ],
    "streams": [
        {
            "codec_name": "h264",
            "codec_type": "video",
            "width": 1080,
            "height": 2400
        },
        {
            "codec_type": "data"
        },
        {
            "codec_type": "data"
        }
    ],
    "format": {
        "duration": "19.787756",
        "size": "2116718"
    }
}

---

### Agent Guide Android Release Quality 2026 10 21 Final Demo Metadata.Json (`docs/agent-guide/android-release-quality/2026-10-21/final-demo-metadata.json`)

{
    "programs": [

    ],
    "stream_groups": [

    ],
    "streams": [
        {
            "codec_name": "h264",
            "codec_type": "video",
            "width": 1080,
            "height": 2400
        },
        {
            "codec_type": "data"
        },
        {
            "codec_type": "data"
        }
    ],
    "format": {
        "duration": "24.829056",
        "size": "1610211"
    }
}

---

### Agent Guide Android Release Quality 2026 10 21 Historical Experiments (`docs/agent-guide/android-release-quality/2026-10-21/historical-experiments.md`)

> Historical record only. Statements below describe superseded candidates and intermediate gates; they are not the accepted final state. Use README.md and the result report for final conclusions.

> Current decision: the root-menu overlay is WITHDRAWN after the user required no extra complexity and unchanged iOS layout/styling. The app and duniverse/lui are back on cd7c7b44. All directories named api33-final/api33-after below refer to historical candidates, not the accepted app. Performance acceptance remains open.

# Android release quality evidence — 2026-10-21

The controlled renderer comparison uses the same original native core in both APKs.
AVD: logseq_android_36, API 36.1, arm64, 1080×2400, 60 Hz, four cores, 3072 MiB RAM, host GPU, headless.
This differs from the earlier QA report’s 2048 MiB configuration; do not combine their metrics.

The fixture is 100 journals × 8 blocks, without WebViews. It is restored to the canonical named graph folder.
Authentication uses an explicitly gated instrumentation fixture with an invented offline token and closed localhost endpoint.
This exercises production UI/storage but does not test real sign-in or sync.
Gradle connected tests remove the app afterward; prepare the offline fixture only after those tests finish.
AndroidOfflineQaFixture runs only with `-e installOfflineFixture true`, checks the dedicated graph ID and confines the endpoint to localhost.

`before/` and `before-3gb/` are INVALID setup attempts: Activity Manager launch timeout / system ANR or wrong foreground, rejected by the driver.
`before-valid/` was rejected by a measurement-tool issue: API 36 exposes current focus under `dumpsys window displays`, not `windows`; a new regression covers it.
`before-valid-v2/` is the original Release renderer after fixture restoration, with packaged library Baseline Profiles installed and speed-profile compilation.
No performance measurement is taken while this task’s Gradle builds are running.
Raw frame statistics, foreground window, middle/before/after UI trees and memory are retained per run.
A hierarchy dump occurs after the fourth gesture to prove list movement; the same driver is used on both sides.

RED/GREEN test logs preserve actual failing behavior and subsequent passing tests.
The added dependencies carry their upstream notices under android/app/src/main/assets/licenses.

`before-clean/` repeats the original renderer with both ART profiles cleared, then installs only its packaged library profile and compiles speed-profile. `after-no-profile/` uses the same reset/install/compile sequence with the renderer fixes and offline math/code helpers. The packaged app-specific profile will be evaluated separately.
`apks.json` records APK sizes/checksums and matching core/JNI checksums for the controlled comparison.

The evaluated app Baseline Profile (NOT shipped) contains 2,740 observed owner-filtered rules from production offline startup and journal scrolling. It includes app/LUI, AndroidComposeView, and the exercised math/highlight engines. It does not use a wildcard to compile every app method. Regenerate after dependency/compiler changes using the official manual collection flow: install an unminified non-debuggable Release, broadcast SKIP_FILE (EXTRA_SKIP_FILE_OPERATION=WRITE_SKIP_FILE), compile verify, clear app profiles, exercise startup/scroll, SAVE_PROFILE, force-stop, `pm dump-profiles --dump-classes-and-methods com.logseq.app`, then pull `/data/misc/profman/com.logseq.app-primary.prof.txt`. Filter by the declaring owner in each ART rule to the same packages and write `android/app/src/main/baseline-prof.txt`. Build again and verify INSTALL_PROFILE returns 1, compile speed-profile, and inspect the installed dexopt state.

Reference: https://developer.android.com/topic/performance/baselineprofiles/manually-create-measure

## Accepted comparison

API 36 attempts above remain diagnostic history. Repeated system/ART startup ANRs made that environment unsuitable for the final comparison; they are not application ANRs.

Final controlled AVD: `logseq_release_qa_33`, serial `emulator-5556`, API 33 Google APIs arm64, 1080×2400, density 420, 60 Hz, four cores, 2048 MiB RAM, host GPU, headless, radios disabled. Builds and this task's iOS simulator were stopped during timing. Offline graph/auth setup is the same as above. Other host applications were not terminated, so absolute timings still reflect a shared Mac.

- `api33-before/`: original Release; compile --reset, packaged library INSTALL_PROFILE, speed-profile. 33/986 janky frames (3.35%).
- `api33-after/`: renderer and root-menu overlay with the candidate app profile. 33/971 (3.40%). This candidate is not shipped.
- `api33-final/`: accepted renderer/root-menu implementation without the candidate app profile. 33/939 (3.51%).

Each directory preserves setup, all three scroll rounds, app foreground and actual list movement, idle/startup logs, memory and separate 12-second Perfetto captures. The before/after content differs because the fix restores missing text and lays out real formula/code content; gestures and stored graph are identical, not pixel-identical workloads. No statistically established jank improvement is claimed.

`evaluated-baseline-prof.txt` preserves the rejected candidate, not an active build input. The final app uses normal dependency-provided profiles. Do not attribute the profile candidate's trace results to the accepted artifact.

Withdrawn LUI experiment: https://github.com/logseq/lui/commit/d01cb95cb0de3bdd2710093ce9b0ed739cc00a18 ; `logseq.opam` no longer pins this commit. Android-only Kotlin changes leave the Apple package revision unchanged.

## Integration build

The final integration build uses root revision `a616c08f60101e95b55e8a8da36abfd8a53eeaa3` plus this task's working changes. Native core was rebuilt rather than skipped. Debug, AndroidTest and Release assembled successfully; all 23 instrumentation tests passed again with this rebuilt core (`final-integration-instrumentation.log`). The controlled comparisons intentionally retain the original core; their measurements must not be relabelled as integration-build performance. `apks.json` records both.

The first demo attempt is excluded: after the date-heading tap, Back left the app, so subsequent coordinates did not reach Settings. Its files have the `demo-attempt-` prefix and are not a Settings pass.

`api33-renderer-only/` isolates restored rich content with the original LUI hierarchy: 73/797 janky frames (9.16%), strongly affected by its second-round outlier. This does not establish a stable speedup for any candidate.

Accepted simplified build: `simplified-integration-build.log` and `simplified-integration-instrumentation.log` show successful Debug/AndroidTest/Release assembly and 17 passing retained tests after restoring LUI cd7c7b44. `apks.json` -> `simplified-integration` records the locally signed APK. Other work in the shared checkout continues to modify core/Apple files; this task does not own or revert those changes.

`android-after-demo.mp4` is the accepted simplified integration APK's native `adb shell screenrecord` output, unedited, 1080×2400 H.264, 19.787756 seconds, no audio. It focuses on restored rich content and bidirectional scrolling. `android-after.png` and `demo-after-hierarchy.xml` verify the production journal UI. Recording is separate from timing.

`api33-profile-only/`: corrected renderer and original LUI with the candidate app profile; 86/799 janky frames (10.76%), no stable benefit. The candidate remains rejected. `simplified-trace/` captures the accepted renderer/original-LUI integration build separately; it is not a timing run from the original binary-controlled pair.

---

### Agent Guide Ui Review 2026 10 22 Evidence Build Artifacts.Json (`docs/agent-guide/ui-review-2026-10-22/evidence/build-artifacts.json`)

{
  "android/app/build/outputs/apk/debug/app-debug.apk": {
    "sha256": "d83dd0b13363b77c8cc6883fd55b54a4449050a2617d2d47119e544d682bf7fa",
    "bytes": 43430661
  },
  "android/app/src/main/jniLibs/arm64-v8a/liblogseq_core.so": {
    "sha256": "0855c7ee8e72fd99109946ee8b5ef2b817ee46583a383d618967186dc19cda86",
    "bytes": 18276864
  }
}

---

### Agent Guide Ui Review 2026 10 22 Evidence Enter After Ime.Json (`docs/agent-guide/ui-review-2026-10-22/evidence/enter-after-ime.json`)

{
  "fixture_blocks": 500,
  "warmup_enters": 3,
  "input_mode": "ime",
  "burst_enters": 20,
  "native_editor_instances": 1,
  "samples_ms": [
    68,
    57,
    65,
    62,
    105,
    101,
    43,
    88,
    59,
    71,
    68,
    65
  ],
  "median_ms": 66.5,
  "p95_ms": 105,
  "measurement": "Injected Enter down to focused editor in the live Compose semantics tree; no accessibility IPC polling",
  "label": "after",
  "build_revision": "94fcee3c",
  "fixture_sha256": "089d1ce138ae9b77332a464153f3f332c7e2f6061c04201efbe1673e60f77942",
  "persisted_splits": 35,
  "apk_sha256": "5b87ae204553b94cfcd4c6fe5e7c3183ec63fd9b4e10b8eb69ddbea91164333f",
  "source_commit": "eddb347",
  "apk_build_revision_note": "Measured APK built from this source before its commit, so BuildConfig records parent 94fcee3c."
}

---

### Agent Guide Ui Review 2026 10 22 Evidence Enter After Key Run1.Json (`docs/agent-guide/ui-review-2026-10-22/evidence/enter-after-key-run1.json`)

{
  "fixture_blocks": 500,
  "warmup_enters": 3,
  "input_mode": "key",
  "burst_enters": 20,
  "native_editor_instances": 1,
  "samples_ms": [
    79,
    85,
    107,
    147,
    88,
    77,
    71,
    105,
    92,
    93,
    104,
    90
  ],
  "median_ms": 91,
  "p95_ms": 147,
  "measurement": "Injected Enter down to focused editor in the live Compose semantics tree; no accessibility IPC polling",
  "label": "after",
  "build_revision": "94fcee3c",
  "fixture_sha256": "089d1ce138ae9b77332a464153f3f332c7e2f6061c04201efbe1673e60f77942",
  "persisted_splits": 35,
  "apk_sha256": "5b87ae204553b94cfcd4c6fe5e7c3183ec63fd9b4e10b8eb69ddbea91164333f",
  "source_commit": "eddb347",
  "apk_build_revision_note": "Measured APK built from this source before its commit, so BuildConfig records parent 94fcee3c."
}

---

### Agent Guide Ui Review 2026 10 22 Evidence Enter After Key.Json (`docs/agent-guide/ui-review-2026-10-22/evidence/enter-after-key.json`)

{
  "fixture_blocks": 500,
  "warmup_enters": 3,
  "input_mode": "key",
  "burst_enters": 20,
  "native_editor_instances": 1,
  "samples_ms": [
    84,
    80,
    108,
    126,
    73,
    56,
    88,
    113,
    88,
    88,
    78,
    86
  ],
  "median_ms": 87,
  "p95_ms": 126,
  "measurement": "Injected Enter down to focused editor in the live Compose semantics tree; no accessibility IPC polling",
  "label": "after",
  "build_revision": "94fcee3c",
  "fixture_sha256": "089d1ce138ae9b77332a464153f3f332c7e2f6061c04201efbe1673e60f77942",
  "persisted_splits": 35,
  "apk_sha256": "5b87ae204553b94cfcd4c6fe5e7c3183ec63fd9b4e10b8eb69ddbea91164333f",
  "source_commit": "eddb347",
  "apk_build_revision_note": "Measured APK built from this source before its commit, so BuildConfig records parent 94fcee3c."
}

---

### Agent Guide Ui Review 2026 10 22 Evidence Enter Before.Json (`docs/agent-guide/ui-review-2026-10-22/evidence/enter-before.json`)

{
  "fixture_blocks": 500,
  "warmup_enters": 3,
  "burst_enters": 20,
  "native_editor_instances": 16,
  "samples_ms": [
    123,
    160,
    115,
    117,
    103,
    118,
    121,
    104,
    125,
    128,
    104,
    102
  ],
  "median_ms": 117.5,
  "p95_ms": 160,
  "measurement": "Injected Enter down to focused editor in the live Compose semantics tree; no accessibility IPC polling",
  "label": "before",
  "build_revision": "42db0ec2",
  "fixture_sha256": "089d1ce138ae9b77332a464153f3f332c7e2f6061c04201efbe1673e60f77942",
  "input_mode": "key",
  "persisted_splits": 24,
  "acceptance": "Failed: native editor recreated and 11 burst inputs not persisted"
}

---

### Agent Guide Ui Review 2026 10 22 Evidence Jvm Test Summary.Json (`docs/agent-guide/ui-review-2026-10-22/evidence/jvm-test-summary.json`)

[
  {
    "suite": "com.logseq.app.AndroidAppEntryParserTest",
    "tests": 1,
    "failures": 0,
    "errors": 0,
    "skipped": 0,
    "cases": [
      "preservesDecodedDeepLinkFieldsForTheCore"
    ]
  },
  {
    "suite": "com.logseq.app.AndroidAudioTranscriberTest",
    "tests": 2,
    "failures": 0,
    "errors": 0,
    "skipped": 0,
    "cases": [
      "emptyRecognitionUpdatesDoNotCreateTranscriptWhitespace",
      "finalSegmentsReplaceTheirPrecedingPartialResult"
    ]
  },
  {
    "suite": "com.logseq.app.AndroidGraphLocalStorageTest",
    "tests": 2,
    "failures": 0,
    "errors": 0,
    "skipped": 0,
    "cases": [
      "spaced directories migrate with assets and readable unicode names",
      "named storage migrates legacy graphs and retains offline state"
    ]
  },
  {
    "suite": "com.logseq.app.AndroidGraphSnapshotDownloaderTest",
    "tests": 2,
    "failures": 0,
    "errors": 0,
    "skipped": 0,
    "cases": [
      "gzipDownloadReportsDecodingAndRejectedDownloadCleansArtifacts",
      "downloads the shared snapshot plan and preserves binary bytes"
    ]
  },
  {
    "suite": "com.logseq.app.AndroidPlatformServicesTest",
    "tests": 7,
    "failures": 0,
    "errors": 0,
    "skipped": 0,
    "cases": [
      "absolute asset paths remain unchanged",
      "asset titles cannot escape the Assets directory",
      "relative assets resolve against the app files directory",
      "configured base URL is trimmed and preserved",
      "shared Android paths resolve relative uploads against app storage",
      "stored session inspection does not require token decryption",
      "blank base URL falls back to the local backend"
    ]
  },
  {
    "suite": "com.logseq.app.AndroidRuntimeLogTest",
    "tests": 2,
    "failures": 0,
    "errors": 0,
    "skipped": 0,
    "cases": [
      "encodes the LG runtime log payload and escapes messages",
      "bounds filters and orders runtime records like iOS"
    ]
  },
  {
    "suite": "com.logseq.app.AuthenticationActivityContractTest",
    "tests": 5,
    "failures": 0,
    "errors": 0,
    "skipped": 0,
    "cases": [
      "oauth callback remains routed to the authentication activity",
      "android app is distinguishable from the existing Logseq app",
      "debug builds allow user configured LAN development backends",
      "cleartext networking is limited to the local development backend",
      "oauth callback returns to the existing authentication activity"
    ]
  },
  {
    "suite": "com.logseq.app.CognitoOAuthTest",
    "tests": 6,
    "failures": 0,
    "errors": 0,
    "skipped": 0,
    "cases": [
      "callback returns the authorization code after state validation",
      "callback surfaces provider errors and missing codes",
      "authorization URL uses PKCE and the configured callback",
      "callback rejects mismatched state",
      "callback rejects unrelated deep links",
      "form encoding is stable and escapes reserved characters"
    ]
  },
  {
    "suite": "com.logseq.app.LuiHexColorTest",
    "tests": 3,
    "failures": 0,
    "errors": 0,
    "skipped": 0,
    "cases": [
      "cssAlphaRemainsAtTheEndForBothLengths",
      "opaqueRgbDoesNotRotateItsChannelsIntoAlpha",
      "invalidColorsAreRejected"
    ]
  },
  {
    "suite": "com.logseq.app.NativeEditorInputPolicyTest",
    "tests": 1,
    "failures": 0,
    "errors": 0,
    "skipped": 0,
    "cases": [
      "executesOnlyDeclaredRulesUsingUtf16Offsets"
    ]
  },
  {
    "suite": "com.logseq.app.OutlinerDropPlacementTest",
    "tests": 5,
    "failures": 0,
    "errors": 0,
    "skipped": 0,
    "cases": [
      "droppingOnTheLeftCreatesASibling",
      "shortBlocksAndZeroHeightUseSafeBoundaries",
      "longBlocksKeepACompactInsertionZone",
      "draggingRightNestsTheBlock",
      "droppingNearTheTopInsertsBeforeEvenWhenIndented"
    ]
  },
  {
    "suite": "com.logseq.app.runtime.AndroidAppEntryQueueTest",
    "tests": 2,
    "failures": 0,
    "errors": 0,
    "skipped": 0,
    "cases": [
      "restartRestoresOrderedSharesAndAcknowledgesOnlySuccess",
      "cancellationRetainsStableCaptureIdentifierAndAttachmentBytesOutsideCache"
    ]
  },
  {
    "suite": "com.logseq.app.runtime.AndroidGraphAssetLoaderTest",
    "tests": 3,
    "failures": 0,
    "errors": 0,
    "skipped": 0,
    "cases": [
      "failedDownloadCanRetryAndCachesExactBytesWithGraphOwnership",
      "graphSwitchRejectionDoesNotLeakTemporaryFiles",
      "cachedImageDoesNotDownload"
    ]
  },
  {
    "suite": "com.logseq.app.runtime.AndroidGraphEventsSyncTest",
    "tests": 1,
    "failures": 0,
    "errors": 0,
    "skipped": 0,
    "cases": [
      "core retry command defers snapshot even without a local cursor"
    ]
  },
  {
    "suite": "com.logseq.app.runtime.AndroidGraphLifecycleTest",
    "tests": 2,
    "failures": 0,
    "errors": 0,
    "skipped": 0,
    "cases": [
      "resync downloads a fresh snapshot without resetting an active editor",
      "deferred snapshot import cleans its download and preserves the core failure"
    ]
  },
  {
    "suite": "com.logseq.app.runtime.AndroidPendingSyncPumpTest",
    "tests": 1,
    "failures": 0,
    "errors": 0,
    "skipped": 0,
    "cases": [
      "native pump waits for the core retry command"
    ]
  },
  {
    "suite": "com.logseq.app.runtime.AndroidPendingSyncTest",
    "tests": 2,
    "failures": 0,
    "errors": 0,
    "skipped": 0,
    "cases": [
      "composerAttachmentPersistsThroughTheLocalCoreEffect",
      "interruptedUploadRetriesTheSameFileAndIdentity"
    ]
  },
  {
    "suite": "com.logseq.app.runtime.CoreBootstrapTest",
    "tests": 1,
    "failures": 0,
    "errors": 0,
    "skipped": 0,
    "cases": [
      "cachedEncryptedGraphOpensOnlyWhenTheCoreHasUnlockedIt"
    ]
  },
  {
    "suite": "com.logseq.app.runtime.CoreEffectExecutorTest",
    "tests": 2,
    "failures": 0,
    "errors": 0,
    "skipped": 0,
    "cases": [
      "automatic pending sync reaches the platform upload pump",
      "native deletion confirmation uses the shared core event"
    ]
  },
  {
    "suite": "com.logseq.app.runtime.LuiDispatchTest",
    "tests": 1,
    "failures": 0,
    "errors": 0,
    "skipped": 0,
    "cases": [
      "viewportWidthCrossesTheNativeEventBoundary"
    ]
  },
  {
    "suite": "com.logseq.app.runtime.LuiPatchFilterTest",
    "tests": 9,
    "failures": 0,
    "errors": 0,
    "skipped": 0,
    "cases": [
      "presentationRulesRemainSpecificToNodeKindAndValue",
      "droppedNodeIdsCanBeReusedWithDifferentPresentationRules",
      "rejectedBatchDoesNotCommitFilterBookkeeping",
      "malformedInputLeavesTheNextValidGenerationUsable",
      "extensionRegistrationTextAndDefaultRestorationPassThrough",
      "emptyBatchStillAdvancesTheBackendGeneration",
      "ignoredRemovalThenAllowedReplacementAndRemovalStayInOrder",
      "iconButtonsKeepTheExistingCenteredGlyphBehavior",
      "unsupportedHintsDoNotHideAllowedTextOrUnicode"
    ]
  },
  {
    "suite": "logseq.app.AndroidE2EECryptoTests",
    "tests": 4,
    "failures": 0,
    "errors": 0,
    "skipped": 0,
    "cases": [
      "privateKeyDecryptionUsesPBKDF2HMACSHA256",
      "secureRandomReturnsTheRequestedNumberOfBytes",
      "rsaOAEPGraphKeyRoundTripAcceptsLogseqKeyEncodings",
      "aesGCMRoundTripMatchesTheNativeBridgeContract"
    ]
  }
]

---

### Agent Guide Ui Review 2026 10 22 Evidence Mobile Publication.Json (`docs/agent-guide/ui-review-2026-10-22/evidence/mobile-publication.json`)

{
  "project_id": "appgprj_6ad94e5ee8308191999228070e61c95a",
  "source_commit": "aa5e9016cbfee92017f585d458a12e39d078840e",
  "version_id": "appgprj_6ad94e5ee8308191999228070e61c95a~appgver_8fa1738a7e048191846833d0d342732f",
  "deployment_id": "appgdep_6ad96f87ed58819184c426c850698681",
  "status": "succeeded",
  "url": "https://logseq-mobile-ux-review-20261022.tiensonqin.chatgpt.site",
  "updated_at": "2026-10-22T02:06:09.902153+00:00"
}

---

### Agent Guide Ui Review 2026 10 22 Evidence Report Browser Check.Json (`docs/agent-guide/ui-review-2026-10-22/evidence/report-browser-check.json`)

{
  "report": "../008-android-ios-ui-ux-review_report.html",
  "viewports": [
    [
      1280,
      800
    ],
    [
      390,
      844
    ]
  ],
  "horizontalOverflow": false,
  "uniqueScreenshots": 46,
  "renderedScreenshotInstances": 46,
  "imageDecodeFailures": 0,
  "filters": {
    "P1": [
      "UX-01",
      "UX-03"
    ]
  },
  "lightbox": {
    "open": true,
    "escapeCloses": true
  },
  "localLinks": "all 94 targets exist",
  "applicationCodeChangedByReview": true,
  "note": "Original review was read-only; follow-up fixes are authorized. Smooth scrolling was disabled only in browser QA to stabilize automated hit coordinates."
}

---

### Agent Guide Ui Review 2026 10 22 Evidence Screenshot Manifest.Json (`docs/agent-guide/ui-review-2026-10-22/evidence/screenshot-manifest.json`)

[
  {
    "file": "assets/android-01-journals.png",
    "sha256": "2fd7d012d66df72c97c7de70e4f244ee23c11a27cd8f051fa70926d791849844",
    "bytes": 182442
  },
  {
    "file": "assets/android-03-attachment.png",
    "sha256": "e2a279a74263a7714c81ac197341cdd6cf08c4e0d0770ea7d1088773c231a711",
    "bytes": 206441
  },
  {
    "file": "assets/android-05-dismiss.png",
    "sha256": "8f1b91657ebdd47384575947d83c64e2afbaa1e65721f191f2d5edf04bd6348c",
    "bytes": 182314
  },
  {
    "file": "assets/android-06-sidebar.png",
    "sha256": "0ab3f1de918ecf6b77d4c974a82c9e4783b41f6c4fabce033cbeaea839a49633",
    "bytes": 97303
  },
  {
    "file": "assets/android-07-settings.png",
    "sha256": "832139bb9d4075e923f81085986ae8b119fd94419ddfec8613f5e45dee97bb02",
    "bytes": 99128
  },
  {
    "file": "assets/android-08-theme-menu.png",
    "sha256": "394dabc5a8674f0b9a469d6ff895c00f4d3f2daf6aa53f57d0e5ec52faf2c1db",
    "bytes": 103427
  },
  {
    "file": "assets/android-09-graphs.png",
    "sha256": "6ba1be1896b4e4fe4d25ce6728d34f0afa13d4de04bf927bfdf722bbf2970e32",
    "bytes": 52993
  },
  {
    "file": "assets/android-10-add-graph-expanded.png",
    "sha256": "a57ce870c42668c7f355525e39d3b4c7c2f106e0a8b25340dee80fd4bd906c2c",
    "bytes": 57936
  },
  {
    "file": "assets/android-10-add-graph.png",
    "sha256": "6471d6791cfce79bdfab6e5677578963700db5fb75877a69a917ac7b3901610a",
    "bytes": 78627
  },
  {
    "file": "assets/android-11-calendar.png",
    "sha256": "fba86c63c6aaab9e8722b0d4e5d3b5d94b9fc50d633b88854643d056f56f7c15",
    "bytes": 136420
  },
  {
    "file": "assets/android-12-editor.png",
    "sha256": "5a940efe31ba8b807d6d2193f0bfa12ed7ef79efdf829b089bae52401738d6b6",
    "bytes": 228493
  },
  {
    "file": "assets/android-13-editor-trailing.png",
    "sha256": "fea1aa1d9092eba2922a4e483f9175ee1f56b460fbe7bdf3c42aca25eca34473",
    "bytes": 208020
  },
  {
    "file": "assets/android-16-search.png",
    "sha256": "16c3be3de16148ced1342d7c0fc9f82a145cf84b1ec1bcc53f2575a436b6ec0f",
    "bytes": 194762
  },
  {
    "file": "assets/android-17-gallery.png",
    "sha256": "3eb7db38f4ef73ffadb5ed26f1cd727b194431ffb2384a22590a9a02131ec5c6",
    "bytes": 24334
  },
  {
    "file": "assets/android-24-plain-longpress.png",
    "sha256": "9abed732c612a72e6593bfa5446b70da5ca9ba6d665e63ccccb06d8c40d6a4aa",
    "bytes": 121007
  },
  {
    "file": "assets/android-25-recording.png",
    "sha256": "a8993a34b49d56d5232e132a70d5cfe6d2c06b1ab8a477ce84bf1f0f1c9599bd",
    "bytes": 131683
  },
  {
    "file": "assets/android-30-current-capture-hit.png",
    "sha256": "02369cb5a528902a64b7a8eef1e559ebfe3227ebbf4168740da05313fdf6633b",
    "bytes": 25513
  },
  {
    "file": "assets/android-32-card-question.png",
    "sha256": "a498f68df12ca3367dc3ad7b7d8c9f0405cafcd24fad8169bdbe6ccebbd87d75",
    "bytes": 46414
  },
  {
    "file": "assets/android-33-card-cloze.png",
    "sha256": "ff99d7432dd0f9c78d7e7b42a42173bca2ef2bf827b353921491edfa7abb064e",
    "bytes": 47690
  },
  {
    "file": "assets/android-34-card-answer.png",
    "sha256": "3c2e71c663e2676858f9b31fcca51827aeaa3ea7edfdf7984bd7a7a4470a0306",
    "bytes": 60182
  },
  {
    "file": "assets/android-35-flashcard-complete.png",
    "sha256": "bf22a91684d0d358a1962ed51d087f336084c73697bb03f7c4ef137c7e8d6fec",
    "bytes": 52454
  },
  {
    "file": "assets/android-fixed-capture.png",
    "sha256": "618f23d2137415e27fc47dcb9c82cb91dc36b7b5eec1b6a49d3658d24c0d6174",
    "bytes": 169814
  },
  {
    "file": "assets/android-fixed-graph-initial.png",
    "sha256": "c0abe34a74d34947e21bc0a15e5284c26d8e5381083265a9a7d567a09370d345",
    "bytes": 92061
  },
  {
    "file": "assets/android-fixed-graph-keyboard.png",
    "sha256": "159caf7a917be829c7d756b455bb8acc201304b3bbf5a8a252dd49f6f3b46135",
    "bytes": 130925
  },
  {
    "file": "assets/android-fixed-selection.png",
    "sha256": "d1e9ff175c25bb8453f712a83ad5fecfc9bc39422d6bc0f24c3b758f6f6a2e97",
    "bytes": 159147
  },
  {
    "file": "assets/android-fixed-settings-dark.png",
    "sha256": "e835dcf4c2506be3dd3016b6326d0cc776cbd72646e71e2afe7b1daf60b529c3",
    "bytes": 100707
  },
  {
    "file": "assets/android-fixed-settings-light.png",
    "sha256": "dea354b1b02cefe68a46e7c35dee16ba3a6d15c6bdb5c9b892d219adba60d974",
    "bytes": 99577
  },
  {
    "file": "assets/android-fixed-sync.png",
    "sha256": "db0c8f8b8da8d0e22c56845832eb22c5df508ab8590960478a4db5e4921c7b0d",
    "bytes": 152148
  },
  {
    "file": "assets/ios-01-journals.png",
    "sha256": "28d0d9312d4617e798a51d9a8243ced7cc9bbaea0f400cd4016c5af34acf91fb",
    "bytes": 1756338
  },
  {
    "file": "assets/ios-03-attachment.png",
    "sha256": "56ee8dc907d191463992c1859ba31e5e2651ea83f536de95d7ad722e6b65977d",
    "bytes": 1380063
  },
  {
    "file": "assets/ios-05-dismiss.png",
    "sha256": "cd8ad765c2118f786a847d777cac629f3a3875c96c62a4835e553a723afd808b",
    "bytes": 1499361
  },
  {
    "file": "assets/ios-06-sidebar.png",
    "sha256": "6ddc20c07658c561c966fcfd484ea7d3ca3287ae2897b26b5684c7c12ddf5a7b",
    "bytes": 355031
  },
  {
    "file": "assets/ios-07-settings.png",
    "sha256": "431f94e91fc3551d6d16e23fc24c5e5fdd31e5fe2a2733396ca418500ae059fd",
    "bytes": 223267
  },
  {
    "file": "assets/ios-08-theme-menu.png",
    "sha256": "1d30a47f48b80a30ccf10cfdfbb85a7c74e73f71128494814b56e8c869e56760",
    "bytes": 279250
  },
  {
    "file": "assets/ios-09-graphs.png",
    "sha256": "e39987ca490a2c2dae086d4470cb27fcbdc358aec449de3a43a38f6ded0cc039",
    "bytes": 128359
  },
  {
    "file": "assets/ios-10-add-graph.png",
    "sha256": "b5ecdb90603a67e9d23831bc623a55ead11932e5ae87d00e44b95b2de871b087",
    "bytes": 147187
  },
  {
    "file": "assets/ios-11-calendar.png",
    "sha256": "3dc97988b0ac7b414f02e137b8a30ef9d2c0f55c6a3ebe50fd9fc1cc7501e555",
    "bytes": 659812
  },
  {
    "file": "assets/ios-12-editor.png",
    "sha256": "94c08fe532d9d45f5815d7f6bca06462cad30b681a84172e754a15f77448e276",
    "bytes": 395035
  },
  {
    "file": "assets/ios-13-editor-trailing.png",
    "sha256": "4f9f661b4a3075c7e8a6e32d3c6c8a7bd8956c7f6b6fcfa3038ef9b0cb7aa8a8",
    "bytes": 394696
  },
  {
    "file": "assets/ios-14-selection.png",
    "sha256": "646f7e0f887eeb7f1a1768d43dc3a2ca7c488db088a953326b2198d429c9d35a",
    "bytes": 1506412
  },
  {
    "file": "assets/ios-16-search.png",
    "sha256": "58b940d55260d4ff95be34ac14cc1d660d085c905ce98ab11f407cc0e7b202c4",
    "bytes": 287996
  },
  {
    "file": "assets/ios-17-gallery.png",
    "sha256": "2408aeac817ad27cc58d71506e82e4ad0125a2efb33ef86c5f2540e15dd70f62",
    "bytes": 2027370
  },
  {
    "file": "assets/ios-25-recording.png",
    "sha256": "9775fad0dab320f9392ddb469624f75fd7b234daa0b14a25de54adace8cb8148",
    "bytes": 130102
  },
  {
    "file": "assets/ios-27-flashcards.png",
    "sha256": "5d9843517a2feb144acc9d3310b095edc1330860a7aa1ac72630ba4469dbb4cc",
    "bytes": 127946
  },
  {
    "file": "assets/ios-28-flashcard-reveal.png",
    "sha256": "4fa661c85b35d951db554c798a9165955ee8158d6380210615a9ff3c5be8938a",
    "bytes": 135097
  },
  {
    "file": "assets/ios-31-flashcard-answer.png",
    "sha256": "1c29986c320142f71e14d1244f203b9cbececa7baa046b5cfa81dce68ba6d3f6",
    "bytes": 160570
  }
]

---

### Agent Guide Ui Review 2026 10 22 Evidence Test Summary.Json (`docs/agent-guide/ui-review-2026-10-22/evidence/test-summary.json`)

{
  "rows": [
    [
      "JVM",
      "64 / 64 通过，0 failure / error / skip",
      "真实 HTTP、缓存、磁盘队列、生命周期、输入偏好等"
    ],
    [
      "常规 Android 安装回归",
      "78 项通过",
      "独立 Compose 控件、图库、rich content、输入、系统 share / recording 等"
    ],
    [
      "真实 native import",
      "1 项带正确 fixture 参数重跑通过",
      "当前重建的 core 输出 importing / opening 与正数记录总量"
    ],
    [
      "真实 IME / installed App",
      "7 项分别独立 instrumentation 运行通过",
      "菜单 / 键盘 / 光标、任务状态、编辑后的数学；整组串行准备仍有 6 项失败，保留限制"
    ],
    [
      "后台同步",
      "6 / 6 通过",
      "本地端点、真实 core、磁盘游标、重试 / 取消 / lease；0.197 秒"
    ],
    [
      "Maestro 双端探索",
      "实际操作与 PNG / hierarchy 证据",
      "发现 4 项交互问题；不是“全产品功能测试全部通过”"
    ]
  ],
  "initialRun": {
    "tests": 86,
    "passed": 78,
    "skipped": 7,
    "failed": 1
  },
  "optInGroupedRun": {
    "tests": 8,
    "passed": 2,
    "failed": 6
  },
  "isolatedIme": 7,
  "nativeImportRetry": 1,
  "backgroundSync": 6
}

---

### Agent Guide Ui Review 2026 10 22 Evidence Verification Summary.Json (`docs/agent-guide/ui-review-2026-10-22/evidence/verification-summary.json`)

{
  "source_commit": "eddb347",
  "ui_fix_commit": "94fcee3",
  "lui_commit": "e3ed35af33f6c0c1965cd719327ee4dd67c47d0f",
  "environment": "API 33 arm64 debug emulator-5556; dedicated UI Regression graph only",
  "core": {
    "tests": 904,
    "passed": 904,
    "seconds": 43.838,
    "log": "core-final.log"
  },
  "android_jvm": {
    "passed": 64,
    "failed": 0,
    "skipped": 0
  },
  "lui_jvm": {
    "passed": 29,
    "failed": 0,
    "skipped": 0
  },
  "installed_ui": {
    "passed": 61,
    "failed": 0,
    "skipped": 0,
    "seconds": 125.687,
    "verifyComposerIme": true,
    "log": "ui-final.log"
  },
  "installed_broad": {
    "passed": 61,
    "failed": 0,
    "skipped": 0,
    "seconds": 66.193,
    "log": "ui-broad-final.log"
  },
  "enter_performance": {
    "hardware_rounds": [
      "enter-after-key-run1.json",
      "enter-after-key.json"
    ],
    "native_ime": "enter-after-ime.json",
    "each_round_persisted": 35,
    "each_round_native_editors": 1
  },
  "teardown_race": {
    "log": "ui-final-teardown-race.log",
    "details": "First combined run passed test bodies but two cleanup actions raced native popup dismissal. Cleanup now waits for dismissal; combined rerun passed all 61."
  },
  "screenshots": "screenshot-manifest.json",
  "ios_changes": "No Apple app or renderer sources, layout or styles changed. Shared core retains the active journal page during editing on both platforms.",
  "limits": [
    "Debug emulator timing is not Redmi or signed Release performance.",
    "No new cloud E2EE/account end-to-end certification.",
    "12 measured latency samples per round do not characterize long-run tail latency."
  ]
}

---

### Agent Guide Android Ios Release Qa 2026 10 21 Release 100x8 Android Performance.Json (`docs/agent-guide/android-ios-release-qa/2026-10-21/release-100x8/android-performance.json`)

{
  "device": "emulator-5554",
  "variant": "release (not debuggable; locally signed with Android debug key)",
  "launches": [
    {
      "first_ui_rendered": 7564.0,
      "authentication_published": 10436.0,
      "am_total_ms": 11250
    },
    {
      "first_ui_rendered": 9574.0,
      "journals_ui_ready": 12145.0,
      "authentication_published": 12301.0,
      "am_total_ms": 10626
    },
    {
      "first_ui_rendered": 3299.0,
      "authentication_published": 3800.0,
      "journals_ui_ready": 4342.0,
      "am_total_ms": 4105
    }
  ],
  "idle_10s": {
    "frames": 0,
    "janky_frames": 0,
    "p50_ms": 4950,
    "p95_ms": 4950,
    "p99_ms": 4950,
    "slow_ui_thread": 0
  },
  "scroll_1": {
    "frames": 130,
    "janky_frames": 61,
    "p50_ms": 34,
    "p95_ms": 150,
    "p99_ms": 200,
    "slow_ui_thread": 53,
    "jank_percent": 46.92
  },
  "scroll_2": {
    "frames": 108,
    "janky_frames": 67,
    "p50_ms": 48,
    "p95_ms": 200,
    "p99_ms": 250,
    "slow_ui_thread": 57,
    "jank_percent": 62.04
  },
  "scroll_3": {
    "frames": 211,
    "janky_frames": 59,
    "p50_ms": 26,
    "p95_ms": 150,
    "p99_ms": 200,
    "slow_ui_thread": 49,
    "jank_percent": 27.96
  }
}

---

### Agent Guide Android Ios Release Qa 2026 10 21 Release System Anr Invalid Android Performance.Json (`docs/agent-guide/android-ios-release-qa/2026-10-21/release-system-anr-invalid/android-performance.json`)

{
  "device": "emulator-5554",
  "variant": "release (not debuggable; locally signed with Android debug key)",
  "launches": [
    {
      "first_ui_rendered": 1759.0,
      "authentication_published": 2010.0,
      "journals_ui_ready": 2560.0,
      "am_total_ms": 2033
    },
    {
      "first_ui_rendered": 802.0,
      "authentication_published": 1048.0,
      "journals_ui_ready": 1303.0,
      "am_total_ms": 980
    },
    {
      "first_ui_rendered": 650.0,
      "authentication_published": 793.0,
      "journals_ui_ready": 982.0,
      "am_total_ms": 850
    }
  ],
  "idle_10s": {
    "frames": 0,
    "janky_frames": 0,
    "p50_ms": 4950,
    "p95_ms": 4950,
    "p99_ms": 4950,
    "slow_ui_thread": 0
  },
  "scroll_1": {
    "frames": 0,
    "janky_frames": 0,
    "p50_ms": 4950,
    "p95_ms": 4950,
    "p99_ms": 4950,
    "slow_ui_thread": 0
  },
  "scroll_2": {
    "frames": 0,
    "janky_frames": 0,
    "p50_ms": 4950,
    "p95_ms": 4950,
    "p99_ms": 4950,
    "slow_ui_thread": 0
  },
  "scroll_3": {
    "frames": 0,
    "janky_frames": 0,
    "p50_ms": 4950,
    "p95_ms": 4950,
    "p99_ms": 4950,
    "slow_ui_thread": 0
  }
}

---

### Agent Guide Android M3 Quality 2026 10 22 Before Android Performance.Json (`docs/agent-guide/android-m3-quality/2026-10-22/before/android-performance.json`)

{
  "device": "emulator-5556",
  "variant": "release",
  "launches": [
    {
      "first_ui_rendered": 546.0,
      "authentication_published": 639.0,
      "journals_ui_ready": 721.0,
      "am_total_ms": 621
    },
    {
      "first_ui_rendered": 511.0,
      "authentication_published": 597.0,
      "journals_ui_ready": 667.0,
      "am_total_ms": 572
    },
    {
      "first_ui_rendered": 556.0,
      "authentication_published": 642.0,
      "journals_ui_ready": 706.0,
      "am_total_ms": 639
    }
  ],
  "idle_10s": {
    "frames": 0,
    "janky_frames": 0,
    "p50_ms": 4950,
    "p95_ms": 4950,
    "p99_ms": 4950,
    "slow_ui_thread": 0
  },
  "scroll_1": {
    "frames": 323,
    "janky_frames": 8,
    "p50_ms": 18,
    "p95_ms": 21,
    "p99_ms": 34,
    "slow_ui_thread": 2,
    "jank_percent": 2.48
  },
  "scroll_2": {
    "frames": 329,
    "janky_frames": 4,
    "p50_ms": 18,
    "p95_ms": 21,
    "p99_ms": 30,
    "slow_ui_thread": 1,
    "jank_percent": 1.22
  },
  "scroll_3": {
    "frames": 321,
    "janky_frames": 10,
    "p50_ms": 18,
    "p95_ms": 24,
    "p99_ms": 53,
    "slow_ui_thread": 3,
    "jank_percent": 3.12
  }
}

---

### Agent Guide Android M3 Quality 2026 10 22 Composer Ime Debug Verification.Json (`docs/agent-guide/android-m3-quality/2026-10-22/composer-ime-debug/verification.json`)

{
  "device": "emulator-5556",
  "android_api": 33,
  "fixture_graph": "uiregression",
  "apk_sha256": "bcb899eb578e7c69ace64cba4c8cc8942bed8de157fae9ffd1ac538b0d9f429e",
  "native_core_sha256": "1a53e7abfcbb62a2079fa2bde395b6ed709dad06a2e8ea9dd4b0536741a0801b",
  "real_ime_checks": [
    {
      "name": "attachmentMenuKeepsTheKeyboardAndBackClosesOnlyTheMenu",
      "passed": true
    },
    {
      "name": "openingAMenuDoesNotForceAnAlreadyHiddenKeyboardToOpen",
      "passed": true
    },
    {
      "name": "outsideMenuTapPreservesTheKeyboardAndDraft",
      "passed": true
    },
    {
      "name": "statusMenuKeepsTheRealKeyboardAndComposerPosition",
      "passed": true
    },
    {
      "name": "statusSelectionPreservesTheInputCaret",
      "passed": true
    }
  ],
  "native_regression": {
    "cases": 72,
    "passed": true,
    "seconds": 205.323
  },
  "host_jvm_cases": 57,
  "standalone_lui_cases": 29,
  "screenshots": [
    "/tmp/logseq-m3-composer-plus-ime.png",
    "/tmp/logseq-m3-composer-status-ime.png"
  ],
  "scope": "Functional UI verification; not a latency or performance benchmark",
  "lui_main_commit": "ab4c1e2d20d9b3ef9caa7451c5b5ecceb0e6d9a3",
  "post_validation_change": "List-item indentation only; Android assemble and 57 host JVM cases pass after formatting."
}

---

### Agent Guide Android M3 Quality 2026 10 22 Native Surfaces Release Android Performance.Json (`docs/agent-guide/android-m3-quality/2026-10-22/native-surfaces-release/android-performance.json`)

{
  "device": "emulator-5556",
  "variant": "release",
  "launches": [
    {
      "first_ui_rendered": 339.0,
      "journals_ui_ready": 672.0,
      "authentication_published": 687.0,
      "am_total_ms": 652
    },
    {
      "first_ui_rendered": 219.0,
      "authentication_published": 306.0,
      "journals_ui_ready": 362.0,
      "am_total_ms": 286
    },
    {
      "first_ui_rendered": 241.0,
      "authentication_published": 311.0,
      "journals_ui_ready": 365.0,
      "am_total_ms": 321
    }
  ],
  "idle_10s": {
    "frames": 0,
    "janky_frames": 0,
    "p50_ms": 4950,
    "p95_ms": 4950,
    "p99_ms": 4950,
    "slow_ui_thread": 0
  },
  "scroll_1": {
    "frames": 334,
    "janky_frames": 5,
    "p50_ms": 18,
    "p95_ms": 23,
    "p99_ms": 32,
    "slow_ui_thread": 2,
    "jank_percent": 1.5
  },
  "scroll_2": {
    "frames": 308,
    "janky_frames": 10,
    "p50_ms": 19,
    "p95_ms": 22,
    "p99_ms": 32,
    "slow_ui_thread": 4,
    "jank_percent": 3.25
  },
  "scroll_3": {
    "frames": 332,
    "janky_frames": 6,
    "p50_ms": 18,
    "p95_ms": 22,
    "p99_ms": 24,
    "slow_ui_thread": 0,
    "jank_percent": 1.81
  }
}

---

### Agent Guide Android M3 Quality 2026 10 22 R8 Baseline A Android Performance.Json (`docs/agent-guide/android-m3-quality/2026-10-22/r8-baseline-a/android-performance.json`)

{
  "device": "emulator-5556",
  "variant": "release",
  "launches": [
    {
      "first_ui_rendered": 1902.0,
      "authentication_published": 2353.0,
      "journals_ui_ready": 2555.0,
      "am_total_ms": 2184
    },
    {
      "first_ui_rendered": 1399.0,
      "journals_ui_ready": 1986.0,
      "authentication_published": 2030.0,
      "am_total_ms": 1610
    },
    {
      "first_ui_rendered": 1808.0,
      "authentication_published": 2132.0,
      "journals_ui_ready": 2313.0,
      "am_total_ms": 2243
    },
    {
      "first_ui_rendered": 1797.0,
      "authentication_published": 2157.0,
      "journals_ui_ready": 2379.0,
      "am_total_ms": 2057
    },
    {
      "first_ui_rendered": 2386.0,
      "journals_ui_ready": 2999.0,
      "authentication_published": 3042.0,
      "am_total_ms": 2795
    }
  ],
  "idle_10s": {
    "frames": 0,
    "janky_frames": 0,
    "p50_ms": 4950,
    "p95_ms": 4950,
    "p99_ms": 4950,
    "slow_ui_thread": 0
  },
  "scroll_1": {
    "frames": 203,
    "janky_frames": 47,
    "p50_ms": 24,
    "p95_ms": 150,
    "p99_ms": 200,
    "slow_ui_thread": 41,
    "jank_percent": 23.15
  },
  "scroll_2": {
    "frames": 345,
    "janky_frames": 15,
    "p50_ms": 20,
    "p95_ms": 34,
    "p99_ms": 53,
    "slow_ui_thread": 9,
    "jank_percent": 4.35
  },
  "scroll_3": {
    "frames": 248,
    "janky_frames": 35,
    "p50_ms": 21,
    "p95_ms": 65,
    "p99_ms": 400,
    "slow_ui_thread": 27,
    "jank_percent": 14.11
  },
  "valid_for_comparison": false,
  "exclusion_reason": "Concurrent Xcode/asset build observed on the host during scrolling; retain raw evidence but do not compare optimization results against it."
}

---

### Agent Guide Android Release Quality 2026 10 21 After No Profile Android Performance.Json (`docs/agent-guide/android-release-quality/2026-10-21/after-no-profile/android-performance.json`)

{
  "device": "emulator-5554",
  "variant": "release",
  "launches": [
    {
      "first_ui_rendered": 629.0,
      "journals_ui_ready": 1393.0,
      "authentication_published": 1545.0,
      "am_total_ms": 963
    },
    {
      "first_ui_rendered": 383.0,
      "authentication_published": 546.0,
      "journals_ui_ready": 632.0,
      "am_total_ms": 644
    },
    {
      "first_ui_rendered": 320.0,
      "authentication_published": 460.0,
      "journals_ui_ready": 599.0,
      "am_total_ms": 512
    }
  ],
  "idle_10s": {
    "frames": 0,
    "janky_frames": 0,
    "p50_ms": 4950,
    "p95_ms": 4950,
    "p99_ms": 4950,
    "slow_ui_thread": 0
  },
  "scroll_1": {
    "frames": 171,
    "janky_frames": 28,
    "p50_ms": 30,
    "p95_ms": 150,
    "p99_ms": 400,
    "slow_ui_thread": 19,
    "jank_percent": 16.37
  },
  "scroll_2": {
    "frames": 174,
    "janky_frames": 64,
    "p50_ms": 32,
    "p95_ms": 150,
    "p99_ms": 200,
    "slow_ui_thread": 47,
    "jank_percent": 36.78
  },
  "scroll_3": {
    "frames": 296,
    "janky_frames": 38,
    "p50_ms": 23,
    "p95_ms": 48,
    "p99_ms": 150,
    "slow_ui_thread": 18,
    "jank_percent": 12.84
  }
}

---

### Agent Guide Android Release Quality 2026 10 21 After Profile Android Performance.Json (`docs/agent-guide/android-release-quality/2026-10-21/after-profile/android-performance.json`)

{
  "device": "emulator-5554",
  "variant": "release",
  "launches": [
    {
      "first_ui_rendered": 475.0,
      "authentication_published": 721.0,
      "journals_ui_ready": 792.0,
      "am_total_ms": 822
    },
    {
      "first_ui_rendered": 673.0,
      "authentication_published": 1168.0,
      "journals_ui_ready": 1374.0,
      "am_total_ms": 1009
    },
    {
      "first_ui_rendered": 942.0,
      "authentication_published": 1464.0,
      "journals_ui_ready": 1647.0,
      "am_total_ms": 1807
    }
  ],
  "idle_10s": {
    "frames": 0,
    "janky_frames": 0,
    "p50_ms": 4950,
    "p95_ms": 4950,
    "p99_ms": 4950,
    "slow_ui_thread": 0
  },
  "scroll_1": {
    "frames": 149,
    "janky_frames": 62,
    "p50_ms": 36,
    "p95_ms": 350,
    "p99_ms": 650,
    "slow_ui_thread": 52,
    "jank_percent": 41.61
  },
  "scroll_2": {
    "frames": 172,
    "janky_frames": 46,
    "p50_ms": 32,
    "p95_ms": 133,
    "p99_ms": 350,
    "slow_ui_thread": 37,
    "jank_percent": 26.74
  },
  "scroll_3": {
    "frames": 252,
    "janky_frames": 58,
    "p50_ms": 27,
    "p95_ms": 65,
    "p99_ms": 150,
    "slow_ui_thread": 46,
    "jank_percent": 23.02
  }
}

---

### Agent Guide Android Release Quality 2026 10 21 Api33 After Android Performance.Json (`docs/agent-guide/android-release-quality/2026-10-21/api33-after/android-performance.json`)

{
  "device": "emulator-5556",
  "variant": "release",
  "launches": [
    {
      "first_ui_rendered": 184.0,
      "authentication_published": 287.0,
      "journals_ui_ready": 351.0,
      "am_total_ms": 297
    },
    {
      "first_ui_rendered": 246.0,
      "authentication_published": 377.0,
      "journals_ui_ready": 435.0,
      "am_total_ms": 346
    },
    {
      "first_ui_rendered": 330.0,
      "authentication_published": 502.0,
      "journals_ui_ready": 564.0,
      "am_total_ms": 581
    }
  ],
  "idle_10s": {
    "frames": 0,
    "janky_frames": 0,
    "p50_ms": 4950,
    "p95_ms": 4950,
    "p99_ms": 4950,
    "slow_ui_thread": 0
  },
  "scroll_1": {
    "frames": 324,
    "janky_frames": 12,
    "p50_ms": 18,
    "p95_ms": 23,
    "p99_ms": 48,
    "slow_ui_thread": 4,
    "jank_percent": 3.7
  },
  "scroll_2": {
    "frames": 319,
    "janky_frames": 10,
    "p50_ms": 18,
    "p95_ms": 21,
    "p99_ms": 32,
    "slow_ui_thread": 6,
    "jank_percent": 3.13
  },
  "scroll_3": {
    "frames": 328,
    "janky_frames": 11,
    "p50_ms": 18,
    "p95_ms": 22,
    "p99_ms": 46,
    "slow_ui_thread": 5,
    "jank_percent": 3.35
  }
}

---

### Agent Guide Android Release Quality 2026 10 21 Api33 Before Android Performance.Json (`docs/agent-guide/android-release-quality/2026-10-21/api33-before/android-performance.json`)

{
  "device": "emulator-5556",
  "variant": "release",
  "launches": [
    {
      "first_ui_rendered": 294.0,
      "authentication_published": 718.0,
      "journals_ui_ready": 776.0,
      "am_total_ms": 656
    },
    {
      "first_ui_rendered": 185.0,
      "authentication_published": 277.0,
      "journals_ui_ready": 348.0,
      "am_total_ms": 276
    },
    {
      "first_ui_rendered": 178.0,
      "authentication_published": 285.0,
      "journals_ui_ready": 366.0,
      "am_total_ms": 267
    }
  ],
  "idle_10s": {
    "frames": 0,
    "janky_frames": 0,
    "p50_ms": 4950,
    "p95_ms": 4950,
    "p99_ms": 4950,
    "slow_ui_thread": 0
  },
  "scroll_1": {
    "frames": 324,
    "janky_frames": 15,
    "p50_ms": 19,
    "p95_ms": 26,
    "p99_ms": 89,
    "slow_ui_thread": 8,
    "jank_percent": 4.63
  },
  "scroll_2": {
    "frames": 331,
    "janky_frames": 10,
    "p50_ms": 17,
    "p95_ms": 20,
    "p99_ms": 26,
    "slow_ui_thread": 2,
    "jank_percent": 3.02
  },
  "scroll_3": {
    "frames": 331,
    "janky_frames": 8,
    "p50_ms": 17,
    "p95_ms": 20,
    "p99_ms": 22,
    "slow_ui_thread": 0,
    "jank_percent": 2.42
  }
}

---

### Agent Guide Android Release Quality 2026 10 21 Api33 Final Android Performance.Json (`docs/agent-guide/android-release-quality/2026-10-21/api33-final/android-performance.json`)

{
  "device": "emulator-5556",
  "variant": "release",
  "launches": [
    {
      "first_ui_rendered": 240.0,
      "journals_ui_ready": 586.0,
      "authentication_published": 617.0,
      "am_total_ms": 399
    },
    {
      "first_ui_rendered": 204.0,
      "authentication_published": 336.0,
      "journals_ui_ready": 385.0,
      "am_total_ms": 306
    },
    {
      "first_ui_rendered": 189.0,
      "authentication_published": 353.0,
      "journals_ui_ready": 452.0,
      "am_total_ms": 284
    }
  ],
  "idle_10s": {
    "frames": 0,
    "janky_frames": 0,
    "p50_ms": 4950,
    "p95_ms": 4950,
    "p99_ms": 4950,
    "slow_ui_thread": 0
  },
  "scroll_1": {
    "frames": 296,
    "janky_frames": 11,
    "p50_ms": 18,
    "p95_ms": 28,
    "p99_ms": 150,
    "slow_ui_thread": 4,
    "jank_percent": 3.72
  },
  "scroll_2": {
    "frames": 328,
    "janky_frames": 8,
    "p50_ms": 18,
    "p95_ms": 21,
    "p99_ms": 23,
    "slow_ui_thread": 0,
    "jank_percent": 2.44
  },
  "scroll_3": {
    "frames": 315,
    "janky_frames": 14,
    "p50_ms": 18,
    "p95_ms": 20,
    "p99_ms": 40,
    "slow_ui_thread": 2,
    "jank_percent": 4.44
  }
}

---

### Agent Guide Android Release Quality 2026 10 21 Api33 Profile Only Android Performance.Json (`docs/agent-guide/android-release-quality/2026-10-21/api33-profile-only/android-performance.json`)

{
  "device": "emulator-5556",
  "variant": "release",
  "launches": [
    {
      "first_ui_rendered": 631.0,
      "journals_ui_ready": 2091.0,
      "authentication_published": 2121.0,
      "am_total_ms": 1274
    },
    {
      "first_ui_rendered": 657.0,
      "authentication_published": 1059.0,
      "journals_ui_ready": 1415.0,
      "am_total_ms": 1139
    },
    {
      "first_ui_rendered": 302.0,
      "authentication_published": 464.0,
      "journals_ui_ready": 532.0,
      "am_total_ms": 447
    }
  ],
  "idle_10s": {
    "frames": 0,
    "janky_frames": 0,
    "p50_ms": 4950,
    "p95_ms": 4950,
    "p99_ms": 4950,
    "slow_ui_thread": 0
  },
  "scroll_1": {
    "frames": 173,
    "janky_frames": 53,
    "p50_ms": 28,
    "p95_ms": 150,
    "p99_ms": 150,
    "slow_ui_thread": 45,
    "jank_percent": 30.64
  },
  "scroll_2": {
    "frames": 291,
    "janky_frames": 25,
    "p50_ms": 20,
    "p95_ms": 69,
    "p99_ms": 133,
    "slow_ui_thread": 18,
    "jank_percent": 8.59
  },
  "scroll_3": {
    "frames": 335,
    "janky_frames": 8,
    "p50_ms": 18,
    "p95_ms": 22,
    "p99_ms": 48,
    "slow_ui_thread": 5,
    "jank_percent": 2.39
  }
}

---

### Agent Guide Android Release Quality 2026 10 21 Api33 Renderer Only Android Performance.Json (`docs/agent-guide/android-release-quality/2026-10-21/api33-renderer-only/android-performance.json`)

{
  "device": "emulator-5556",
  "variant": "release",
  "launches": [
    {
      "first_ui_rendered": 201.0,
      "authentication_published": 335.0,
      "journals_ui_ready": 426.0,
      "am_total_ms": 387
    },
    {
      "first_ui_rendered": 178.0,
      "authentication_published": 276.0,
      "journals_ui_ready": 328.0,
      "am_total_ms": 244
    },
    {
      "first_ui_rendered": 171.0,
      "authentication_published": 251.0,
      "journals_ui_ready": 294.0,
      "am_total_ms": 244
    }
  ],
  "idle_10s": {
    "frames": 0,
    "janky_frames": 0,
    "p50_ms": 4950,
    "p95_ms": 4950,
    "p99_ms": 4950,
    "slow_ui_thread": 0
  },
  "scroll_1": {
    "frames": 323,
    "janky_frames": 11,
    "p50_ms": 17,
    "p95_ms": 20,
    "p99_ms": 65,
    "slow_ui_thread": 4,
    "jank_percent": 3.41
  },
  "scroll_2": {
    "frames": 152,
    "janky_frames": 45,
    "p50_ms": 21,
    "p95_ms": 300,
    "p99_ms": 500,
    "slow_ui_thread": 32,
    "jank_percent": 29.61
  },
  "scroll_3": {
    "frames": 322,
    "janky_frames": 17,
    "p50_ms": 19,
    "p95_ms": 34,
    "p99_ms": 89,
    "slow_ui_thread": 12,
    "jank_percent": 5.28
  }
}

---

### Agent Guide Android Release Quality 2026 10 21 Before Clean Android Performance.Json (`docs/agent-guide/android-release-quality/2026-10-21/before-clean/android-performance.json`)

{
  "device": "emulator-5554",
  "variant": "release",
  "launches": [
    {
      "first_ui_rendered": 2009.0,
      "authentication_published": 2815.0,
      "journals_ui_ready": 3061.0,
      "am_total_ms": 2934
    },
    {
      "first_ui_rendered": 454.0,
      "authentication_published": 764.0,
      "journals_ui_ready": 1024.0,
      "am_total_ms": 987
    },
    {
      "first_ui_rendered": 320.0,
      "authentication_published": 460.0,
      "journals_ui_ready": 558.0,
      "am_total_ms": 476
    }
  ],
  "idle_10s": {
    "frames": 0,
    "janky_frames": 0,
    "p50_ms": 4950,
    "p95_ms": 4950,
    "p99_ms": 4950,
    "slow_ui_thread": 0
  },
  "scroll_1": {
    "frames": 224,
    "janky_frames": 53,
    "p50_ms": 30,
    "p95_ms": 117,
    "p99_ms": 250,
    "slow_ui_thread": 42,
    "jank_percent": 23.66
  },
  "scroll_2": {
    "frames": 200,
    "janky_frames": 51,
    "p50_ms": 29,
    "p95_ms": 133,
    "p99_ms": 250,
    "slow_ui_thread": 40,
    "jank_percent": 25.5
  },
  "scroll_3": {
    "frames": 92,
    "janky_frames": 52,
    "p50_ms": 48,
    "p95_ms": 400,
    "p99_ms": 1750,
    "slow_ui_thread": 42,
    "jank_percent": 56.52
  }
}

---

### Agent Guide Android Release Quality 2026 10 21 Before Valid V2 Android Performance.Json (`docs/agent-guide/android-release-quality/2026-10-21/before-valid-v2/android-performance.json`)

{
  "device": "emulator-5554",
  "variant": "release",
  "launches": [
    {
      "first_ui_rendered": 967.0,
      "journals_ui_ready": 1731.0,
      "authentication_published": 1772.0,
      "am_total_ms": 1469
    },
    {
      "first_ui_rendered": 1222.0,
      "authentication_published": 1667.0,
      "journals_ui_ready": 1921.0,
      "am_total_ms": 1566
    },
    {
      "first_ui_rendered": 348.0,
      "authentication_published": 489.0,
      "journals_ui_ready": 585.0,
      "am_total_ms": 539
    }
  ],
  "idle_10s": {
    "frames": 0,
    "janky_frames": 0,
    "p50_ms": 4950,
    "p95_ms": 4950,
    "p99_ms": 4950,
    "slow_ui_thread": 0
  },
  "scroll_1": {
    "frames": 201,
    "janky_frames": 34,
    "p50_ms": 17,
    "p95_ms": 129,
    "p99_ms": 200,
    "slow_ui_thread": 21,
    "jank_percent": 16.92
  },
  "scroll_2": {
    "frames": 189,
    "janky_frames": 61,
    "p50_ms": 30,
    "p95_ms": 150,
    "p99_ms": 650,
    "slow_ui_thread": 50,
    "jank_percent": 32.28
  },
  "scroll_3": {
    "frames": 185,
    "janky_frames": 65,
    "p50_ms": 34,
    "p95_ms": 150,
    "p99_ms": 250,
    "slow_ui_thread": 55,
    "jank_percent": 35.14
  }
}

---

### Agent Guide Android Release Quality 2026 10 21 Final Integration Performance Android Performance.Json (`docs/agent-guide/android-release-quality/2026-10-21/final-integration-performance/android-performance.json`)

{
  "device": "emulator-5556",
  "variant": "release",
  "launches": [
    {
      "first_ui_rendered": 228.0,
      "authentication_published": 313.0,
      "journals_ui_ready": 338.0,
      "am_total_ms": 319
    },
    {
      "first_ui_rendered": 167.0,
      "authentication_published": 266.0,
      "journals_ui_ready": 298.0,
      "am_total_ms": 257
    },
    {
      "first_ui_rendered": 215.0,
      "authentication_published": 338.0,
      "journals_ui_ready": 402.0,
      "am_total_ms": 344
    }
  ],
  "idle_10s": {
    "frames": 0,
    "janky_frames": 0,
    "p50_ms": 4950,
    "p95_ms": 4950,
    "p99_ms": 4950,
    "slow_ui_thread": 0
  },
  "scroll_1": {
    "frames": 324,
    "janky_frames": 11,
    "p50_ms": 18,
    "p95_ms": 26,
    "p99_ms": 73,
    "slow_ui_thread": 7,
    "jank_percent": 3.4
  },
  "scroll_2": {
    "frames": 336,
    "janky_frames": 7,
    "p50_ms": 19,
    "p95_ms": 23,
    "p99_ms": 30,
    "slow_ui_thread": 1,
    "jank_percent": 2.08
  },
  "scroll_3": {
    "frames": 333,
    "janky_frames": 9,
    "p50_ms": 18,
    "p95_ms": 21,
    "p99_ms": 27,
    "slow_ui_thread": 1,
    "jank_percent": 2.7
  }
}

---

### Agent Guide Android Release Quality 2026 10 21 Final Integration Performance Apk.Json (`docs/agent-guide/android-release-quality/2026-10-21/final-integration-performance/apk.json`)

{
  "apk_sha256": "4d657a380c2a80d6066beb81075c2b8c3433b12f98b6e521c447b9a1310a5290",
  "native_sha256": {
    "lib/arm64-v8a/libandroidx.graphics.path.so": "41e9a793c43a0f4fddb19e33f346bace464f30f888ba7b9eaf96294ea115bfb6",
    "lib/arm64-v8a/liblogseq_core.so": "d296f89ec569e49b2c87454cbbb21c766a3d4751efba06f7cb923b5920b021b9",
    "lib/arm64-v8a/liblogseq_jni.so": "dc561c30a0898dfe8dfe4127528679729ee3a6beb69c5096bd7b41c5c59fd7c7"
  },
  "build_revision": "2ad7d02e",
  "lui_revision": "1edf12691bcd78ee65b52355d5c2a35f4194524c"
}

---

### Agent Guide Android Release Quality 2026 10 21 Patch Pipeline After.Json (`docs/agent-guide/android-release-quality/2026-10-21/patch-pipeline/after.json`)

{
  "patch_count": 50,
  "patch_bytes": 8774118,
  "cpu_ms": [
    546.518785,
    704.941581,
    1466.927997,
    1389.190152,
    635.403205,
    537.201081,
    549.569879,
    535.924255,
    458.793612,
    495.822132,
    465.484578,
    528.935119
  ],
  "median_cpu_ms": 546.518785,
  "core_sha256": "51332dec49f00dcf822f44cbc2536ad25723c45f03c9f8f1f8819c86ff539932",
  "stream_sha256": "849c3a1969d03a46b9697c2ce9f87a94e698273af5720bdfef1c7584b47b4530",
  "label": "after"
}

---

### Agent Guide Android Release Quality 2026 10 21 Patch Pipeline Before 1.Json (`docs/agent-guide/android-release-quality/2026-10-21/patch-pipeline/before-1.json`)

{
  "patch_count": 50,
  "patch_bytes": 8774118,
  "cpu_ms": [
    2236.607714,
    2072.619479,
    3914.178909,
    1762.378539,
    1483.173703,
    1720.114097,
    1560.434613,
    1687.7329,
    1685.881356,
    2226.5047,
    1732.479255,
    2599.945198
  ],
  "median_cpu_ms": 1762.378539,
  "core_sha256": "51332dec49f00dcf822f44cbc2536ad25723c45f03c9f8f1f8819c86ff539932",
  "stream_sha256": "849c3a1969d03a46b9697c2ce9f87a94e698273af5720bdfef1c7584b47b4530",
  "label": "before"
}

---

### Agent Guide Android Release Quality 2026 10 21 Patch Pipeline Before 2.Json (`docs/agent-guide/android-release-quality/2026-10-21/patch-pipeline/before-2.json`)

{
  "patch_count": 50,
  "patch_bytes": 8774118,
  "cpu_ms": [
    1243.766463,
    1107.945039,
    2057.723782,
    1329.238657,
    1179.551956,
    1142.517278,
    1422.319677,
    2927.512455,
    1946.053165,
    2675.106043,
    9150.807405,
    7394.301429
  ],
  "median_cpu_ms": 1946.053165,
  "core_sha256": "51332dec49f00dcf822f44cbc2536ad25723c45f03c9f8f1f8819c86ff539932",
  "stream_sha256": "849c3a1969d03a46b9697c2ce9f87a94e698273af5720bdfef1c7584b47b4530",
  "label": "before"
}

---

### Agent Guide Android Release Quality 2026 10 21 Patch Pipeline Before.Json (`docs/agent-guide/android-release-quality/2026-10-21/patch-pipeline/before.json`)

{
  "patch_count": 50,
  "patch_bytes": 8774118,
  "cpu_ms": [
    2236.607714,
    2072.619479,
    3914.178909,
    1762.378539,
    1483.173703,
    1720.114097,
    1560.434613,
    1687.7329,
    1685.881356,
    2226.5047,
    1732.479255,
    2599.945198
  ],
  "median_cpu_ms": 1762.378539,
  "core_sha256": "51332dec49f00dcf822f44cbc2536ad25723c45f03c9f8f1f8819c86ff539932",
  "stream_sha256": "849c3a1969d03a46b9697c2ce9f87a94e698273af5720bdfef1c7584b47b4530",
  "label": "before"
}

---

### Agent Guide Android Release Quality 2026 10 21 Patch Pipeline Controlled Source Differences.Json (`docs/agent-guide/android-release-quality/2026-10-21/patch-pipeline/controlled-source-differences.json`)

{
  "kotlin_differences": [
    "android/app/src/main/kotlin/com/logseq/app/runtime/LogseqRuntime.kt",
    "android/app/src/main/kotlin/com/logseq/app/runtime/LuiPatchFilter.kt"
  ],
  "common_lui_checkout": "/Users/tiensonqin/Codes/projects/logseq-chat/duniverse/lui",
  "native_libraries": "Baseline repacked from after APK to make core/JNI/path libraries byte-identical."
}

---

### Agent Guide Android Release Quality 2026 10 21 Patch Pipeline Final Frozen Apks.Json (`docs/agent-guide/android-release-quality/2026-10-21/patch-pipeline/final-frozen-apks.json`)

{
  "logseq-final-frozen-debug.apk": {
    "sha256": "ae4a947412ec90b5da0a7eb2efa92bd8623e62d251620867a14af04819ff1867",
    "native_sha256": {
      "lib/arm64-v8a/libandroidx.graphics.path.so": "41e9a793c43a0f4fddb19e33f346bace464f30f888ba7b9eaf96294ea115bfb6",
      "lib/arm64-v8a/liblogseq_jni.so": "e9362372c4a93494cc39494838faab180bcd764756fe6e1ecf2265b75175ed1a",
      "lib/arm64-v8a/liblogseq_core.so": "d296f89ec569e49b2c87454cbbb21c766a3d4751efba06f7cb923b5920b021b9"
    }
  },
  "logseq-final-frozen-test.apk": {
    "sha256": "50bce66c49e6bd038258f57857ff59e1e8bf5f1a2f4fc94464d558fd1ad05629",
    "native_sha256": {}
  },
  "logseq-final-frozen-release-unsigned.apk": {
    "sha256": "06a8e33fee9451da69b1e8c745d12680b0b53ce8ef8b77f70996cfcf716e03d6",
    "native_sha256": {
      "lib/arm64-v8a/libandroidx.graphics.path.so": "41e9a793c43a0f4fddb19e33f346bace464f30f888ba7b9eaf96294ea115bfb6",
      "lib/arm64-v8a/liblogseq_core.so": "d296f89ec569e49b2c87454cbbb21c766a3d4751efba06f7cb923b5920b021b9",
      "lib/arm64-v8a/liblogseq_jni.so": "dc561c30a0898dfe8dfe4127528679729ee3a6beb69c5096bd7b41c5c59fd7c7"
    }
  },
  "build_revision": "2ad7d02e"
}

---

### Agent Guide Android Release Quality 2026 10 21 Patch Pipeline Integration Test Apks.Json (`docs/agent-guide/android-release-quality/2026-10-21/patch-pipeline/integration-test-apks.json`)

{
  "app-debug.apk": {
    "sha256": "3be35578a2d9f991a8b3ca7a3a4407e32358e5e6eb98cebeac3af562297feeca",
    "native_sha256": {
      "lib/arm64-v8a/libandroidx.graphics.path.so": "41e9a793c43a0f4fddb19e33f346bace464f30f888ba7b9eaf96294ea115bfb6",
      "lib/arm64-v8a/liblogseq_jni.so": "e9362372c4a93494cc39494838faab180bcd764756fe6e1ecf2265b75175ed1a",
      "lib/arm64-v8a/liblogseq_core.so": "36bb0c6d69fbcbb0ed4a90388db284a2c62e5020d9335d918e78982c86e5a0a8"
    }
  },
  "app-debug-androidTest.apk": {
    "sha256": "9962cd0f9c200849be54f382c20932012b6207299e482172306c2f682ac2b8e4",
    "native_sha256": {}
  }
}

---

### Agent Guide Android Release Quality 2026 10 21 Patch Pipeline Release After.Json (`docs/agent-guide/android-release-quality/2026-10-21/patch-pipeline/release-after.json`)

{
  "patch_count": 50,
  "patch_bytes": 8774118,
  "cpu_ms": [
    221.361826,
    212.643508,
    357.316956,
    338.869998,
    428.419249,
    607.118167,
    428.525332,
    298.656079,
    506.868835,
    448.830915,
    290.903711,
    266.656676
  ],
  "median_cpu_ms": 348.093477,
  "core_sha256": "51332dec49f00dcf822f44cbc2536ad25723c45f03c9f8f1f8819c86ff539932",
  "stream_sha256": "849c3a1969d03a46b9697c2ce9f87a94e698273af5720bdfef1c7584b47b4530",
  "label": "after"
}

---

### Agent Guide Android Release Quality 2026 10 21 Patch Pipeline Release Before 2.Json (`docs/agent-guide/android-release-quality/2026-10-21/patch-pipeline/release-before-2.json`)

{
  "patch_count": 50,
  "patch_bytes": 8774118,
  "cpu_ms": [
    1599.800991,
    802.693719,
    683.17075,
    689.963499,
    709.967785,
    1740.294911,
    851.137535,
    960.750866,
    2106.740968,
    2703.456208,
    1084.668817,
    1053.998368
  ],
  "median_cpu_ms": 1053.998368,
  "core_sha256": "51332dec49f00dcf822f44cbc2536ad25723c45f03c9f8f1f8819c86ff539932",
  "stream_sha256": "849c3a1969d03a46b9697c2ce9f87a94e698273af5720bdfef1c7584b47b4530",
  "label": "before"
}

---

### Agent Guide Android Release Quality 2026 10 21 Patch Pipeline Release Before.Json (`docs/agent-guide/android-release-quality/2026-10-21/patch-pipeline/release-before.json`)

{
  "patch_count": 50,
  "patch_bytes": 8774118,
  "cpu_ms": [
    653.129073,
    565.551377,
    534.686153,
    485.081539,
    487.376709,
    475.533127,
    483.191324,
    478.381533,
    497.37737,
    480.031202,
    537.122875,
    555.158463
  ],
  "median_cpu_ms": 497.37737,
  "core_sha256": "51332dec49f00dcf822f44cbc2536ad25723c45f03c9f8f1f8819c86ff539932",
  "stream_sha256": "849c3a1969d03a46b9697c2ce9f87a94e698273af5720bdfef1c7584b47b4530",
  "label": "before"
}

---

### Agent Guide Android Release Quality 2026 10 21 Patch Pipeline Revisions.Json (`docs/agent-guide/android-release-quality/2026-10-21/patch-pipeline/revisions.json`)

{
  "app_base": "e2a0fcafab94c9a5a2e4183b689e420be01b4ae3",
  "lui": "1edf12691bcd78ee65b52355d5c2a35f4194524c",
  "apple_lui_revision": "cd7c7b44885e61a28510ca0454a409a82f59dfee"
}

---

### Agent Guide Android Release Quality 2026 10 21 Patch Pipeline Summary Provisional.Json (`docs/agent-guide/android-release-quality/2026-10-21/patch-pipeline/summary-provisional.json`)

{
  "before-1": {
    "median_cpu_ms": 1747.428897,
    "samples": 12,
    "stream_sha256": "849c3a1969d03a46b9697c2ce9f87a94e698273af5720bdfef1c7584b47b4530",
    "core_sha256": "51332dec49f00dcf822f44cbc2536ad25723c45f03c9f8f1f8819c86ff539932"
  },
  "after": {
    "median_cpu_ms": 541.859933,
    "samples": 12,
    "stream_sha256": "849c3a1969d03a46b9697c2ce9f87a94e698273af5720bdfef1c7584b47b4530",
    "core_sha256": "51332dec49f00dcf822f44cbc2536ad25723c45f03c9f8f1f8819c86ff539932"
  },
  "before-2": {
    "median_cpu_ms": 1684.1864209999999,
    "samples": 12,
    "stream_sha256": "849c3a1969d03a46b9697c2ce9f87a94e698273af5720bdfef1c7584b47b4530",
    "core_sha256": "51332dec49f00dcf822f44cbc2536ad25723c45f03c9f8f1f8819c86ff539932"
  }
}

---

### Agent Guide Android Release Quality 2026 10 21 Patch Pipeline Summary.Json (`docs/agent-guide/android-release-quality/2026-10-21/patch-pipeline/summary.json`)

{
  "release-before": {
    "median_cpu_ms": 492.3770395,
    "min_cpu_ms": 475.533127,
    "max_cpu_ms": 653.129073,
    "samples": 12,
    "core_sha256": "51332dec49f00dcf822f44cbc2536ad25723c45f03c9f8f1f8819c86ff539932",
    "stream_sha256": "849c3a1969d03a46b9697c2ce9f87a94e698273af5720bdfef1c7584b47b4530"
  },
  "release-after": {
    "median_cpu_ms": 348.093477,
    "min_cpu_ms": 212.643508,
    "max_cpu_ms": 607.118167,
    "samples": 12,
    "core_sha256": "51332dec49f00dcf822f44cbc2536ad25723c45f03c9f8f1f8819c86ff539932",
    "stream_sha256": "849c3a1969d03a46b9697c2ce9f87a94e698273af5720bdfef1c7584b47b4530"
  },
  "release-before-2": {
    "median_cpu_ms": 1007.374617,
    "min_cpu_ms": 683.17075,
    "max_cpu_ms": 2703.456208,
    "samples": 12,
    "core_sha256": "51332dec49f00dcf822f44cbc2536ad25723c45f03c9f8f1f8819c86ff539932",
    "stream_sha256": "849c3a1969d03a46b9697c2ce9f87a94e698273af5720bdfef1c7584b47b4530"
  },
  "before-1": {
    "median_cpu_ms": 1747.428897,
    "min_cpu_ms": 1483.173703,
    "max_cpu_ms": 3914.178909,
    "samples": 12,
    "core_sha256": "51332dec49f00dcf822f44cbc2536ad25723c45f03c9f8f1f8819c86ff539932",
    "stream_sha256": "849c3a1969d03a46b9697c2ce9f87a94e698273af5720bdfef1c7584b47b4530"
  },
  "after": {
    "median_cpu_ms": 541.859933,
    "min_cpu_ms": 458.793612,
    "max_cpu_ms": 1466.927997,
    "samples": 12,
    "core_sha256": "51332dec49f00dcf822f44cbc2536ad25723c45f03c9f8f1f8819c86ff539932",
    "stream_sha256": "849c3a1969d03a46b9697c2ce9f87a94e698273af5720bdfef1c7584b47b4530"
  },
  "before-2": {
    "median_cpu_ms": 1684.1864209999999,
    "min_cpu_ms": 1107.945039,
    "max_cpu_ms": 9150.807405,
    "samples": 12,
    "core_sha256": "51332dec49f00dcf822f44cbc2536ad25723c45f03c9f8f1f8819c86ff539932",
    "stream_sha256": "849c3a1969d03a46b9697c2ce9f87a94e698273af5720bdfef1c7584b47b4530"
  }
}

---

### Agent Guide Android Release Quality 2026 10 21 Patch Ui After Android Performance.Json (`docs/agent-guide/android-release-quality/2026-10-21/patch-ui-after/android-performance.json`)

{
  "device": "emulator-5556",
  "variant": "release",
  "launches": [
    {
      "first_ui_rendered": 516.0,
      "authentication_published": 811.0,
      "journals_ui_ready": 884.0,
      "am_total_ms": 998
    },
    {
      "first_ui_rendered": 991.0,
      "authentication_published": 1275.0,
      "journals_ui_ready": 1337.0,
      "am_total_ms": 2245
    },
    {
      "first_ui_rendered": 342.0,
      "authentication_published": 531.0,
      "journals_ui_ready": 567.0,
      "am_total_ms": 622
    }
  ],
  "idle_10s": {
    "frames": 0,
    "janky_frames": 0,
    "p50_ms": 4950,
    "p95_ms": 4950,
    "p99_ms": 4950,
    "slow_ui_thread": 0
  },
  "scroll_1": {
    "frames": 332,
    "janky_frames": 14,
    "p50_ms": 18,
    "p95_ms": 28,
    "p99_ms": 53,
    "slow_ui_thread": 7,
    "jank_percent": 4.22
  },
  "scroll_2": {
    "frames": 300,
    "janky_frames": 16,
    "p50_ms": 20,
    "p95_ms": 40,
    "p99_ms": 150,
    "slow_ui_thread": 13,
    "jank_percent": 5.33
  },
  "scroll_3": {
    "frames": 317,
    "janky_frames": 29,
    "p50_ms": 20,
    "p95_ms": 53,
    "p99_ms": 89,
    "slow_ui_thread": 22,
    "jank_percent": 9.15
  }
}

---

### Agent Guide Android Release Quality 2026 10 21 Patch Ui After Controlled Apks.Json (`docs/agent-guide/android-release-quality/2026-10-21/patch-ui-after/controlled-apks.json`)

{
  "before": {
    "apk_sha256": "67a88670593aef70df85a44270e2d165ba08fbf9f560b21dfd00c2f4c45eb358",
    "native_sha256": {
      "lib/arm64-v8a/libandroidx.graphics.path.so": "41e9a793c43a0f4fddb19e33f346bace464f30f888ba7b9eaf96294ea115bfb6",
      "lib/arm64-v8a/liblogseq_core.so": "51332dec49f00dcf822f44cbc2536ad25723c45f03c9f8f1f8819c86ff539932",
      "lib/arm64-v8a/liblogseq_jni.so": "dc561c30a0898dfe8dfe4127528679729ee3a6beb69c5096bd7b41c5c59fd7c7"
    }
  },
  "after": {
    "apk_sha256": "e5af038c3beeef94b03c98e934bcf48dd19d1a055294bf4dcdf5bdcd12ef6e28",
    "native_sha256": {
      "lib/arm64-v8a/libandroidx.graphics.path.so": "41e9a793c43a0f4fddb19e33f346bace464f30f888ba7b9eaf96294ea115bfb6",
      "lib/arm64-v8a/liblogseq_core.so": "51332dec49f00dcf822f44cbc2536ad25723c45f03c9f8f1f8819c86ff539932",
      "lib/arm64-v8a/liblogseq_jni.so": "dc561c30a0898dfe8dfe4127528679729ee3a6beb69c5096bd7b41c5c59fd7c7"
    }
  }
}

---

### Agent Guide Android Release Quality 2026 10 21 Patch Ui Before Android Performance.Json (`docs/agent-guide/android-release-quality/2026-10-21/patch-ui-before/android-performance.json`)

{
  "device": "emulator-5556",
  "variant": "release",
  "launches": [
    {
      "first_ui_rendered": 676.0,
      "journals_ui_ready": 1413.0,
      "authentication_published": 1483.0,
      "am_total_ms": 1162
    },
    {
      "first_ui_rendered": 680.0,
      "journals_ui_ready": 1054.0,
      "authentication_published": 1098.0,
      "am_total_ms": 1043
    },
    {
      "first_ui_rendered": 2599.0,
      "journals_ui_ready": 3336.0,
      "authentication_published": 3382.0,
      "am_total_ms": 3877
    }
  ],
  "idle_10s": {
    "frames": 0,
    "janky_frames": 0,
    "p50_ms": 4950,
    "p95_ms": 4950,
    "p99_ms": 4950,
    "slow_ui_thread": 0
  },
  "scroll_1": {
    "frames": 326,
    "janky_frames": 16,
    "p50_ms": 18,
    "p95_ms": 32,
    "p99_ms": 48,
    "slow_ui_thread": 8,
    "jank_percent": 4.91
  },
  "scroll_2": {
    "frames": 252,
    "janky_frames": 23,
    "p50_ms": 19,
    "p95_ms": 38,
    "p99_ms": 150,
    "slow_ui_thread": 14,
    "jank_percent": 9.13
  },
  "scroll_3": {
    "frames": 165,
    "janky_frames": 54,
    "p50_ms": 22,
    "p95_ms": 150,
    "p99_ms": 300,
    "slow_ui_thread": 38,
    "jank_percent": 32.73
  }
}

---

--- METRICS ---
- Files Extracted: 79
- Estimated Token Budget: ~116987 tokens
- Canonical Reference: https://codewiki.google/github.com/logseq/mobile
