diff --git a/AGENTS.md b/AGENTS.md index 5655699..c210366 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,306 +1,308 @@ -# AGENTS.md — VniDrop agent guide +# AGENTS.md -Instructions for AI agents and humans working in this repository. Read this -before making changes. Prefer following existing code over inventing patterns. +Operational instructions for coding agents working in this repository. +Humans: see `README.md` for product overview and run configs. +Agents: read this file (and the nearest nested `AGENTS.md`) before editing. + +Nested guides take precedence when editing under those trees: + +- [`crates/vnidrop/AGENTS.md`](crates/vnidrop/AGENTS.md) — Rust core +- [`shared/AGENTS.md`](shared/AGENTS.md) — Compose Multiplatform UI / KMP --- -## 1. Mission and stack +## Project overview -**VniDrop** is a cross-platform local file-transfer app: share files/folders with -nearby devices via tickets (copy, QR, NFC), with optional sender approval. +VniDrop is a cross-platform **local P2P file transfer** app (Android, iOS, Desktop). -| Layer | Location | Role | -|--------|----------|------| -| Rust core | `crates/vnidrop/` | Iroh networking, blob store, SQLite, approval, tickets, streaming | -| Shared UI | `shared/` | Compose Multiplatform UI, ViewModels, platform bridges | -| Apps | `androidApp/`, `iosApp/`, `desktopApp/` | Thin hosts | -| UniFFI | Gobley + `crates/vnidrop/uniffi.toml` | Kotlin bindings to `VnidropCore` | +| Layer | Path | Responsibility | +|-------|------|----------------| +| Rust core | `crates/vnidrop/` | Iroh endpoint, blobs, SQLite, tickets, approval, streaming | +| Shared KMP | `shared/` | Compose UI, ViewModels, expect/actual platform bridges | +| Hosts | `androidApp/`, `iosApp/`, `desktopApp/` | Thin app shells | -**Non-negotiable product rule:** platform/UI code opens files and reacts to -events; **Rust owns byte streaming**. Do not pull transfer payloads through -Kotlin heap as the default path. +**Invariant:** UI/platform opens files and handles pickers; **Rust streams bytes**. +Do not design features that move transfer payloads through Kotlin heap by default. -Primary docs (do not re-copy wholesale into PRs): +Domain docs (reference, do not paste into PRs): -- Core send/receive flow: [`crates/vnidrop/CORE_FLOW.md`](crates/vnidrop/CORE_FLOW.md) -- Rust test layout: [`crates/vnidrop/tests/README.md`](crates/vnidrop/tests/README.md) +- [`crates/vnidrop/CORE_FLOW.md`](crates/vnidrop/CORE_FLOW.md) +- [`crates/vnidrop/tests/README.md`](crates/vnidrop/tests/README.md) --- -## 2. Hard rules (always) +## Absolute rules -1. **Default to PRs into `master`.** Do not merge locally to `master` unless the - user explicitly asks. -2. **Do not push, force-push, or open a PR** unless the user asks. -3. **Signed commits** when the repo enables signing (`commit.gpgsign`). If - signing fails (empty agent / passphrase), stop and ask the user to unlock - the key (`ssh-add …`). Do not silently create unsigned commits. -4. **Minimal scope.** Change only what the task requires. No drive-by refactors, - dependency bumps, or reformatting unrelated files. -5. **Do not force architecture migrations.** Adapt to existing patterns - (ViewModels, feature packages, Rust modules). Structural rewrites only when - requested or when fixing a clear violation. -6. **Never commit secrets**, API keys, keychains, or passphrases. Never paste - user passphrases into chat or files. -7. **Destructive git** (`reset --hard`, `push --force`, dropping data) only with - explicit user approval. -8. **Bug fixes need tests** at the correct layer (see §6). +1. Prefer PRs into `master`. Do not merge to `master` locally unless the user asks. +2. Do not `git push`, force-push, or open a PR unless the user asks. +3. If `commit.gpgsign` is enabled, create **signed** commits only. If signing fails + (empty `ssh-add -l`), stop and tell the user to unlock the key. Never switch to + unsigned commits to “unblock” yourself. +4. Change only files required for the task. No drive-by refactors, dependency bumps, + or repo-wide formatting. +5. Do not force architecture migrations (MVI, Hilt, Nav3, etc.) unless requested. +6. Never commit secrets, key material, or passphrases. +7. Destructive git (`reset --hard`, `push --force`, dropping DBs) only with explicit + user approval. +8. **Every bug fix includes a regression test** at the lowest layer that catches it. +9. After code changes, run the **relevant** checks in [Build and test](#build-and-test) + and fix failures before finishing. --- -## 3. Compose Multiplatform — use `compose-skill` +## Build and test -### When to load it +Install prerequisites when missing: Rust stable + rustfmt + clippy, JDK 17, +Android NDK/SDK only if building Android, Xcode only for iOS. -For **any** Kotlin UI / presentation work (screens, components, theme, -navigation, resources, ViewModel↔UI wiring, accessibility, lists, animation), -load and follow: +### Rust core (`crates/vnidrop` or workspace root) + +Run from the **repo root** (Cargo workspace): + +```bash +cargo fmt --all -- --check +cargo clippy --workspace --all-targets -- -D warnings +cargo test --workspace --all-targets +``` + +Focused: + +```bash +cargo test -p vnidrop +cargo test -p vnidrop --test output_sink +cargo test -p vnidrop --test transfer +cargo test -p vnidrop --test approval +cargo test -p vnidrop --test lifecycle +``` + +After finishing Rust edits, format: + +```bash +cargo fmt --all +``` + +CI also runs `cargo doc --workspace --no-deps` with `RUSTDOCFLAGS=-D warnings` +(see `.github/workflows/rust-core.yml`). Run it before large Rust public-API changes. + +### Shared KMP / Compose (`shared/`) + +```bash +./gradlew :shared:jvmTest +./gradlew :shared:compileKotlinJvm +``` + +Other targets (slower / machine-dependent): + +```bash +./gradlew :shared:testAndroidHostTest +./gradlew :shared:iosSimulatorArm64Test # macOS + Xcode +./gradlew :androidApp:assembleDebug +./gradlew :desktopApp:run +``` + +**Note:** `jvmTest` CI runs on **macOS** because Gobley host cargo is enabled for +the current Gobley host; Linux JVM cargo may be disabled in +`shared/build.gradle.kts`. Prefer macOS for local parity with CI. + +### What to run before finishing + +| You changed… | Minimum verification | +|--------------|----------------------| +| `crates/vnidrop/**` only | `cargo fmt`, `cargo clippy … -D warnings`, `cargo test -p vnidrop` | +| Cancel / export / sinks | Above + `cargo test -p vnidrop --test output_sink` | +| `shared/**` only | `./gradlew :shared:jvmTest` | +| Both | Rust suite + `:shared:jvmTest` | +| Docs only | No suite required; verify links/paths | + +Do not kill long `cargo` / Gradle runs mid-flight unless they hang past several +minutes with no output; first builds are slow. + +--- + +## Repository map (edit here) + +### Rust runtime (keep split; do not re-merge into one file) + +``` +crates/vnidrop/src/runtime/ + mod.rs # CoreInner, startup recovery, emit helpers + facade.rs # UniFFI VnidropCore + block_on + share.rs # import / share + receive.rs # receive, download, export, output sinks + lifecycle.rs # cancel, delete, status, access mode, shutdown + provider.rs # provider events, per-connection send progress +``` + +Other core modules: `filesystem.rs`, `repository.rs`, `approval.rs`, +`handshake.rs`, `ticket.rs`, `access_policy.rs`, `event_hub.rs`, `api.rs`. + +### Shared app + +``` +shared/src/commonMain/kotlin/com/vnidrop/app/ + core/ # CoreGateway, models, pickers interfaces + feature/send|receive|approvals|settings|app/ + ui/theme|components|navigation|feedback|state/ +androidMain|iosMain|jvmMain/ # expect/actual implementations +``` + +### Platform file rules (do not violate) + +- Desktop / path-based iOS: paths; directory walk in Rust when `is_directory`. +- Android **share**: ParcelFileDescriptor **file** FDs only — never a directory FD. + Folder share expands SAF trees in Kotlin to per-file FDs + relative names. +- Android **receive** default: MediaStore Downloads sink; custom trees via SAF write. +- Receive publish: no-overwrite temp + hard link / exclusive rename + (see `CORE_FLOW.md`). + +--- + +## Code style + +### General + +- Match surrounding code (naming, imports, error handling). +- Prefer small, reviewable diffs. Avoid files growing past ~800 LoC without + splitting when adding substantial logic. +- Do not add one-off helpers used only once if an inline block is clearer. +- Prefer exhaustive `when` / `match`; avoid wildcards that hide new cases. + +### Comments (strict) + +Comment **why**, invariants, and platform/concurrency traps only. + +- Do comment: cancel-before-await ordering, SAF/FD limits, security-scoped + leases, “exactly one finish/abort after start_file”, durability rules. +- Do **not** comment: restating the next line, tutorial narration, section + banners that repeat the function name, pasted docs from this file. + +### Rust + +- Follow Clippy with `-D warnings` (CI fails otherwise). +- Do not hold `std::sync::MutexGuard` or other guards across `.await`. +- Prefer `Handle::block_on` via existing `VnidropCore::block_on` for concurrent + API entry; cancel signals active transfers **synchronously** before async work. +- Prefer private modules; export only what UniFFI / other crates need. +- New public traits/types: short docs when the role is non-obvious. + +### Kotlin / Compose + +For UI and presentation work, **load and follow** the in-repo skill: ```text .codex/skills/compose-skill/SKILL.md ``` -Do **not** invent a parallel Compose style guide. The skill is the source of -truth for Compose/CMP defaults. +- Open at most one `references/*.md` file when the skill’s Quick Routing requires it. +- Do not invent a second Compose style guide. +- VniDrop uses **MVVM-style** ViewModels (`*State` + `StateFlow` + named methods), + not a forced MVI `onEvent` base — adapt, do not rewrite. +- Theme via `LocalVniDropColors` / `VniDropThemeTokens` only. + Brand primary (light): HSL `271, 91%, 65%` ≈ `#A855F7`. +- Strings: CMP `Res.string.*` / composeResources — not Android `R` in `commonMain`. +- Verify multiplatform target support before adding AndroidX/Jetpack deps to + `commonMain`. -### How to use it - -1. Read existing feature code first (conventions beat generic tutorials). -2. Apply the skill’s core rules (state in ViewModel, dumb UI, unidirectional data). -3. For advanced topics only, open **one** file under - `.codex/skills/compose-skill/references/` using the skill’s Quick Routing table - (e.g. `testing.md`, `performance.md`, `cross-platform.md`, `resources.md`). -4. Do **not** load the entire `references/` tree “just in case.” - -### VniDrop-specific overrides (compose-skill adapts; do not “fix” these) - -| Topic | VniDrop convention | -|--------|-------------------| -| Architecture | **MVVM-style** feature ViewModels: immutable `*State` data class, `StateFlow`, **named public methods** (not a mandatory MVI `onEvent` sealed hierarchy). One-shot UI feedback often via shared snackbar/`UiMessage` rather than a formal Effect channel—match the feature you edit. | -| Packages | `com.vnidrop.app.feature.` + `ui/*` + `core/*` | -| Route / Screen split | Prefer thin `*Route` (wiring) + `*Screen` / feature composables (render + callbacks). | -| Theme | Use `LocalVniDropColors` / `VniDropThemeTokens` in `shared/.../ui/theme/VniDropTheme.kt`. Do not hard-code one-off brand colors. **Brand primary (light):** HSL `271, 91%, 65%` ≈ `#A855F7`. | -| Strings / drawables | Compose Multiplatform resources (`composeResources`, `Res.string.*`), not Android `R` in `commonMain`. | -| DI | Follow existing `AppGraph` / construction patterns; do not introduce Hilt/Koin migrations unprompted. | -| Platform code | `expect`/`actual` or interfaces under `androidMain` / `iosMain` / `jvmMain`. Android SAF/tree/FDs and iOS security-scoped URLs stay on the platform side. | - -Compose-skill’s “Existing Project Policy” applies: preserve working structure. +Details: [`shared/AGENTS.md`](shared/AGENTS.md). --- -## 4. Code comments +## Testing instructions -Comments exist for **future readers who already know the language**. +- Prefer deterministic tests (gates, fixed sizes, public API fixtures). +- Avoid long sleeps; if polling is required: short interval + hard timeout + + clear assertion message. +- Rust integration tests use **public** UniFFI API + `tests/support/` only. +- Failure paths: assert durable status and/or events when applicable, not only + the error string. +- Do not add tests for pure static constants. +- Do not add negative tests for code you deleted. +- Prefer comparing whole objects when equality is meaningful. -### Do comment - -- Non-obvious **why** (concurrency, cancel ordering, durability, security). -- Platform traps (SAF cannot pass directory FDs; security-scoped lease lifetime; - MediaStore Downloads vs path probes). -- Invariants and failure modes (“exactly one of finish/abort after start_file”). -- Public or crate-boundary contracts that tests rely on. - -### Do not comment - -- Restating the next line of code (`// increment i`, `// return result`). -- Section banners that only repeat the function name. -- Tutorial-style narration of self-explanatory control flow. -- Large blocks duplicated from `CORE_FLOW.md` or this file. - -Prefer a short module/`//!` doc or one high-signal line over many low-signal lines. -Match the density of existing Rust comments in `crates/vnidrop/src/runtime/` and -Kotlin comments on Android expand/share paths. - ---- - -## 5. Architecture map (where to edit) - -### Rust core (`crates/vnidrop/src/`) - -| Area | Path | -|------|------| -| UniFFI + runtime entry | `runtime/facade.rs` (`VnidropCore`) | -| Share / import | `runtime/share.rs` | -| Receive / export / sinks | `runtime/receive.rs` | -| Cancel / delete / status / access | `runtime/lifecycle.rs` | -| Provider events, send progress per peer | `runtime/provider.rs` | -| Startup, `CoreInner`, emit helpers | `runtime/mod.rs` | -| SQLite | `repository.rs` | -| Paths, import collect, atomic publish | `filesystem.rs` | -| Approval handshake | `approval.rs`, `handshake.rs` | -| Tickets | `ticket.rs` | -| Access policy | `access_policy.rs` | -| Events | `event_hub.rs`, `api.rs` (`CoreEvent`) | - -Runtime was split intentionally: **keep modules focused**; do not reassemble a -monolithic `runtime.rs`. - -### Shared KMP (`shared/src/`) - -| Area | Path | -|------|------| -| Core gateway / models | `commonMain/.../core/` | -| Send UI | `commonMain/.../feature/send/` | -| Receive UI | `commonMain/.../feature/receive/` | -| Approvals | `commonMain/.../feature/approvals/` | -| Settings | `commonMain/.../feature/settings/` | -| Theme / shell / components | `commonMain/.../ui/` | -| Android pickers, SAF, MediaStore, NFC/QR | `androidMain/` | -| iOS pickers, security scope, NFC/QR | `iosMain/` | -| Desktop paths / pickers | `jvmMain/` | - -### Platform file rules (summary) - -- **Desktop / path-based iOS:** `SourceKind::Path` or security-scoped URL→path; - directories walked in Rust when `is_directory`. -- **Android share:** open documents as **FDs**; **never** a directory FD. - Folder share expands SAF trees in Kotlin (`expandShareDirectory`) into - per-file FDs with relative `displayName` paths. -- **Android receive default:** system Downloads via MediaStore sink when - available; custom trees via SAF write sink. -- **Receive publish:** no-overwrite temps + hard link / exclusive rename (see - `CORE_FLOW.md`). - -### Progress / multi-receiver - -- Send-side byte progress is attributed with `endpoint_id` (and `connection_id`) - on provider transfer events; UI aggregates via - `progressForReceiver` / `activeSendProgress` in - `shared/.../ui/state/AppUiModels.kt`. -- Delivery completion is a separate `delivery` phase / receiver request status. - ---- - -## 6. Testing - -### Policy - -- **Every bug fix** includes a regression test at the lowest layer that catches it. -- Prefer **deterministic** setups (latches/gates, fixed sizes) over multi-second - sleeps. If polling is required: short interval + **bounded timeout** + useful - assertion message. -- Integration tests use the **public** UniFFI API (`tests/support/`), not - private internals exposed only for tests. -- Failure tests should check durable status and/or events when relevant, not - only the error string. - -### Where tests live +Layout: | Layer | Location | -|--------|----------| -| Rust unit / crate-private | `crates/vnidrop/src/tests/` | -| Rust integration | `crates/vnidrop/tests/` + `tests/support/` | -| Shared pure logic | `shared/src/commonTest/` | -| Shared Compose / JVM | `shared/src/jvmTest/` | - -See [`crates/vnidrop/tests/README.md`](crates/vnidrop/tests/README.md) for Rust -organization rules. - -### Commands agents should run (as relevant) - -```bash -# Rust format / lint / tests -cargo fmt --all -- --check -cargo clippy --workspace --all-targets -- -D warnings -cargo test -p vnidrop - -# Shared KMP (desktop JVM suite) -./gradlew :shared:jvmTest -``` - -CI (path-filtered): - -- `.github/workflows/rust-core.yml` — fmt, clippy `-D warnings`, tests -- `.github/workflows/shared-kmp.yml` — `jvmTest` (and related shared paths) - -If you change Rust cancel/export concurrency, at least run: - -```bash -cargo test -p vnidrop --test output_sink -``` +|-------|----------| +| Rust unit / private | `crates/vnidrop/src/tests/` | +| Rust integration | `crates/vnidrop/tests/` | +| Shared logic | `shared/src/commonTest/` | +| Shared Compose/JVM | `shared/src/jvmTest/` | --- -## 7. Git and PR conventions +## Git and PR instructions ### Branches -Name after **what changes**, not roadmap steps: +Name the change, not a roadmap step: -- Good: `feat/folder-share`, `fix/cancel-export-hang`, `refactor/split-runtime-module`, `docs/agents-md` -- Bad: `feat/step3-remaining`, `temp`, `wip` +- Good: `feat/folder-share`, `fix/cancel-export-hang`, `docs/agents-md` +- Bad: `feat/step3-remaining`, `wip`, `temp` -Delete merged feature branches **locally and on origin** after the PR lands; -start the next task from updated `master`. +After a PR merges: delete the feature branch **locally and on `origin`**, then +branch from updated `master`. ### Commits -- Prefer conventional style used in history: `feat(scope):`, `fix(scope):`, - `refactor(scope):`, `docs:`, `style:`, `ci:`. -- Subject focuses on **why / user-visible outcome**, complete sentences in the - body when needed. -- Keep commits reviewable; do not mix unrelated features. +- Style in history: `feat(scope):`, `fix(scope):`, `refactor(scope):`, `docs:`, `ci:`. +- Subject = outcome; body only when needed. +- Signed when repo requires it. ### Pull requests - Title matches the main change. -- Summary: short bullets of **what** and **why**. -- **Test plan must be useful for this PR**: - - Exact commands to run, and/or - - 1–2 concrete device/UI scenarios that would catch a regression. -- Avoid generic filler (“CI green”, “test everything”) with no commands or - scenarios. - -### Workflow preferences established in this project - -- User often requires **say-so before push/PR**. -- Prefer cleaning up remote feature branches after merge. -- When SSH signing fails, unblock with agent unlock—not policy bypass. +- Summary: short bullets of what/why. +- **Test plan must be executable for this PR**: + - exact commands, and/or + - 1–2 concrete scenarios that would catch a regression. +- No filler plans (“everything works”, “CI green”) without commands or scenarios. --- -## 8. Implementation checklist (before finishing a task) +## Security considerations -1. Read surrounding code and this file; for UI, load `compose-skill`. -2. Implement the smallest correct change. -3. Add/adjust tests for behavior changes and bug fixes. -4. Run the relevant format/lint/test commands. -5. Comments only where they add non-obvious information. -6. Commit (signed) if asked; push/PR only if asked. -7. Leave a concise summary of what changed and how it was verified. +- Treat tickets and endpoint IDs as sensitive enough not to log full blobs in + production paths. +- Do not weaken approval/access checks for convenience. +- Do not store secrets in the repo; app data dirs and key files stay out of git. +- Be careful with file publish races (no-clobber rename/link policy exists for a reason). --- -## 9. Anti-patterns (do not) - -- Streaming multi-MB transfer data through Kotlin as the primary design. -- Passing Android **directory** FDs into Rust. -- Nested exclusive `Runtime::block_on` patterns that deadlock cancel during - receive (use the existing handle-based entry / sync cancel signal approach). -- Holding locks across `.await` (Clippy `await_holding_lock` fails CI). -- Rebuilding a single 1.7k-line `runtime.rs`. -- Migrating the whole app to MVI/Hilt/Nav3 “because best practice.” -- Adding dependencies without verifying multiplatform target support. -- Flaky tests that sleep for tens of seconds hoping races resolve. -- Unsigned commits when signing is required, or force-push without consent. - ---- - -## 10. Quick file index for common tasks +## Common tasks → start files | Task | Start here | |------|------------| -| Share import / multi-file / folders | `runtime/share.rs`, `filesystem.rs`, platform `FileSystemService.*` | -| Receive path / sink export | `runtime/receive.rs`, Android/iOS sinks | -| Cancel / delete / stop share | `runtime/lifecycle.rs`, `facade.rs` cancel path | -| Per-receiver send progress | `runtime/provider.rs`, `AppUiModels.kt`, `TransferDetails.kt` | -| Approvals UI | `feature/approvals/`, `approval.rs` | -| Ticket / QR / NFC | `TransferShareActions.*`, `ReceiveInvitationActions.*` | -| Theme / brand color | `ui/theme/VniDropTheme.kt` | +| Share / multi-file / folders | `runtime/share.rs`, `filesystem.rs`, platform `FileSystemService.*` | +| Receive / export / sinks | `runtime/receive.rs` | +| Cancel / delete / stop share | `runtime/lifecycle.rs`, `facade.rs` | +| Per-receiver send progress | `runtime/provider.rs`, `ui/state/AppUiModels.kt` | +| Approvals | `feature/approvals/`, `approval.rs` | +| QR / NFC invitations | `TransferShareActions.*`, `ReceiveInvitationActions.*` | +| Theme / brand | `ui/theme/VniDropTheme.kt` | | Compose skill | `.codex/skills/compose-skill/SKILL.md` | --- -*Last aligned with post–PR #10 tree (`runtime/` split, multi-file/folder share, -iOS QR/NFC, per-receiver progress, cancel-export fixes).* +## Anti-patterns (never) + +- Streaming multi-MB transfer data through Kotlin as the primary design +- Passing Android **directory** FDs into Rust +- Nested exclusive `Runtime::block_on` that deadlocks cancel during receive +- Holding locks across `.await` +- Rebuilding a monolithic `runtime.rs` +- Flaky multi-minute sleeps in tests +- Unsigned commits when signing is required +- Force-push or secret commits without explicit user direction + +--- + +## Implementation checklist + +1. Read this file + nearest nested `AGENTS.md`. +2. For Compose/UI: load `compose-skill`. +3. Smallest correct change; tests for bugs/behavior changes. +4. Run relevant build/test commands; fix failures. +5. Sparse comments only where non-obvious. +6. Commit (signed) / push / PR only as the user requests. +7. Summarize what changed and what you ran. diff --git a/crates/vnidrop/AGENTS.md b/crates/vnidrop/AGENTS.md new file mode 100644 index 0000000..ec71e19 --- /dev/null +++ b/crates/vnidrop/AGENTS.md @@ -0,0 +1,128 @@ +# AGENTS.md — `crates/vnidrop` (Rust core) + +Nearest guide when editing under `crates/vnidrop/`. Root [`AGENTS.md`](../../AGENTS.md) +still applies; this file wins for Rust-specific commands and conventions. + +--- + +## Purpose + +This crate is the transfer backend exposed to Kotlin via UniFFI (`VnidropCore`). +It owns Iroh, blobs, SQLite history, tickets, approval handshake, and file streaming. + +Read [`CORE_FLOW.md`](CORE_FLOW.md) before changing send/receive/export/cancel. + +--- + +## Commands (run from repo root) + +Always prefer workspace commands so lockfile/fmt stay consistent: + +```bash +cargo fmt --all +cargo fmt --all -- --check +cargo clippy --workspace --all-targets -- -D warnings +cargo test -p vnidrop +cargo test --workspace --all-targets +``` + +Focused integration suites: + +```bash +cargo test -p vnidrop --test transfer +cargo test -p vnidrop --test approval +cargo test -p vnidrop --test lifecycle +cargo test -p vnidrop --test output_sink +``` + +Docs (CI uses `-D warnings`): + +```bash +RUSTDOCFLAGS='-D warnings' cargo doc -p vnidrop --no-deps +``` + +Run `cargo fmt --all` after finishing Rust edits without asking. + +--- + +## Module layout + +``` +src/ + runtime/ + mod.rs # CoreInner, startup recovery, emit helpers + facade.rs # UniFFI surface, block_on, cancel entry + share.rs # share / import + receive.rs # receive, download, export, OutputSinkFile + lifecycle.rs # cancel share, delete, status, access mode, shutdown + provider.rs # provider messages, per-peer transfer progress + filesystem.rs # collect sources, atomic publish, path rules + repository.rs # SQLite + approval.rs / handshake.rs / ticket.rs / access_policy.rs / event_hub.rs + api.rs # UniFFI records/enums + tests/ # crate-private unit tests +tests/ # public-API integration tests + support/ +``` + +**Do not** reassemble a single huge `runtime.rs`. Prefer new focused modules if a +file approaches ~800 LoC of non-test code. + +--- + +## Hard constraints + +1. **Public API stability:** UniFFI surface changes break Kotlin. Prefer additive + changes; update shared Kotlin call sites in the same change when required. +2. **Streaming stays in Rust.** Platform passes paths or FDs; core does not pull + whole files into Kotlin. +3. **Android FDs are files only.** `SourceKind::FileDescriptor` with + `is_directory=true` must fail; directories are expanded on the platform side. +4. **Cancel:** signal active-transfer oneshot **synchronously** before async DB + work. Use existing `take_active_transfer` / facade cancel path. Do not reintroduce + nested exclusive `Runtime::block_on` deadlocks. +5. **No lock across await:** Clippy `await_holding_lock` fails CI. +6. **ReceiveOutputSink:** after successful `start_file`, exactly one of + `finish_file` or `abort_file` (see `OutputSinkFile` Drop). +7. **No-overwrite publish** for path receives (temp + hard link / exclusive rename). +8. Integration tests must use the **public** API + `tests/support/` only. + +--- + +## Code style (Rust) + +- Clippy clean with `-D warnings`. +- Prefer exhaustive `match`; avoid catch-all arms that hide new enum variants. +- Prefer comparing whole objects in tests when practical. +- Comment only non-obvious why (concurrency, durability, platform FS quirks). +- Do not add one-off private helpers used once if inline is clearer. +- Prefer private modules; export deliberately via `lib.rs` / UniFFI. + +--- + +## Testing + +- Unit / private: `src/tests/` (see crate `tests.rs` paths). +- Integration: `tests/*.rs` + `tests/support/mod.rs` (`TestNode`, `MemoryOutputSink`, + `CoreGuard`, etc.). +- Bug fixes need a regression test. +- Prefer gates/latches over multi-second sleeps (see output_sink cancel test). +- Failure tests: durable status and/or events when applicable. +- Recovery tests: shut down core, reopen same data dir. + +Details: [`tests/README.md`](tests/README.md). + +--- + +## PR / verify checklist for this crate + +```bash +cargo fmt --all -- --check +cargo clippy --workspace --all-targets -- -D warnings +cargo test -p vnidrop +``` + +If you touched cancel, export, or sinks, also: + +```bash +cargo test -p vnidrop --test output_sink +``` diff --git a/shared/AGENTS.md b/shared/AGENTS.md new file mode 100644 index 0000000..26ecea6 --- /dev/null +++ b/shared/AGENTS.md @@ -0,0 +1,126 @@ +# AGENTS.md — `shared/` (Compose Multiplatform + KMP) + +Nearest guide when editing under `shared/`. Root [`AGENTS.md`](../AGENTS.md) +still applies; this file wins for UI/KMP work. + +--- + +## Purpose + +`shared` is the multiplatform app layer: Compose UI, feature ViewModels, and +`expect`/`actual` bridges into Android, iOS, and desktop. Native transfer work +goes through UniFFI `VnidropCore` (see `crates/vnidrop`). + +--- + +## Compose skill (required for UI work) + +For screens, components, theme, navigation, resources, ViewModel↔UI wiring, +lists, animation, accessibility: + +1. Load [`.codex/skills/compose-skill/SKILL.md`](../.codex/skills/compose-skill/SKILL.md). +2. Follow its workflow and defaults. +3. Open **at most one** file under `.codex/skills/compose-skill/references/` when + the skill’s Quick Routing table says you need deeper guidance. +4. Do **not** invent a parallel Compose style guide. + +### Project policy (overrides generic skill defaults) + +| Topic | Do this | +|-------|---------| +| Architecture | Keep **MVVM-style** ViewModels: immutable `*State`, `StateFlow`, **named methods**. Do not force MVI `onEvent` sealed hierarchies unless asked. | +| Structure | Feature packages under `com.vnidrop.app.feature.*`; thin route/wiring + screen/composables. | +| Theme | Only `LocalVniDropColors` / `VniDropThemeTokens` (`ui/theme/VniDropTheme.kt`). Brand primary light ≈ `#A855F7` (HSL 271, 91%, 65%). | +| Strings | CMP composeResources / `Res.string.*` — not Android `R` in `commonMain`. | +| DI | Follow existing `AppGraph` construction; no unprompted Hilt/Koin migration. | +| Platform | `androidMain` / `iosMain` / `jvmMain` for pickers, SAF, security-scoped URLs, NFC/QR. | +| Dependencies | Before adding Jetpack/AndroidX to `commonMain`, verify multiplatform artifacts for all targets. | + +compose-skill “Existing Project Policy”: adapt to this repo; do not force-migrate. + +--- + +## Commands + +From repo root: + +```bash +./gradlew :shared:jvmTest +./gradlew :shared:compileKotlinJvm +``` + +Optional: + +```bash +./gradlew :shared:testAndroidHostTest +./gradlew :shared:iosSimulatorArm64Test +./gradlew :desktopApp:run +./gradlew :androidApp:assembleDebug +``` + +CI `:shared:jvmTest` runs on **macOS** (Gobley host cargo). Prefer macOS for +local parity. + +When Kotlin changes touch UniFFI-generated APIs, rebuild/test with a full +`jvmTest` so Gobley/native pieces stay aligned. + +--- + +## Layout + +``` +src/ + commonMain/kotlin/com/vnidrop/app/ + core/ # models, CoreGateway, FilePicker interfaces + feature/send|receive|approvals|settings|app/ + ui/ # theme, components, navigation, feedback, state helpers + commonMain/composeResources/ + androidMain|iosMain|jvmMain/ + commonTest|jvmTest|... +``` + +### Platform file bridging (must preserve) + +- **Android share:** open content URIs as FDs; expand **folder trees** to per-file + documents with relative `displayName` paths before calling Rust + (`FileSystemService.android.kt` / `expandShareDirectory`). +- **Android receive:** MediaStore Downloads sink and/or SAF tree write sink. +- **iOS:** keep security-scoped leases alive while Rust reads paths. +- **Desktop:** filesystem paths; directories may be marked `isDirectory` for Rust walk. + +Never pass a directory as a single Android FD into `SourceKind.FILE_DESCRIPTOR`. + +--- + +## Code style (Kotlin) + +- Match existing feature style (imports, naming, state updates via `update { }`). +- Composables render state and invoke callbacks; no business rules in `@Composable` + bodies (network, share creation, ticket parse — ViewModel/core). +- Prefer stable list keys from domain IDs. +- Comments only for non-obvious platform or concurrency reasons. +- Do not hard-code brand colors; use theme tokens. + +--- + +## Testing + +- Logic: `src/commonTest/` (e.g. ViewModel fakes, `AppUiModels` progress helpers). +- Compose/JVM: `src/jvmTest/`. +- Add/adjust tests when changing state machines, progress aggregation, or + share/receive eligibility. +- Prefer fakes in `commonTest` support over real UniFFI in pure unit tests. + +```bash +./gradlew :shared:jvmTest +``` + +--- + +## Anti-patterns + +- Streaming transfer bytes through Kotlin as the main design +- Directory FDs on Android +- New DI framework “because best practice” +- Loading every compose-skill reference file for a small UI tweak +- Android `R.string` in `commonMain`