docs: rewrite AGENTS.md to agents.md / Codex style

Make agent instructions imperative and command-first, add nested
crates/vnidrop and shared guides, and keep compose-skill as the UI
source of truth with VniDrop-specific overrides.
This commit is contained in:
2026-07-12 20:26:31 +02:00
parent 4de004cc1f
commit ff8e046cf7
3 changed files with 502 additions and 246 deletions

494
AGENTS.md
View File

@@ -1,306 +1,308 @@
# AGENTS.md — VniDrop agent guide # AGENTS.md
Instructions for AI agents and humans working in this repository. Read this Operational instructions for coding agents working in this repository.
before making changes. Prefer following existing code over inventing patterns. 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 VniDrop is a cross-platform **local P2P file transfer** app (Android, iOS, Desktop).
nearby devices via tickets (copy, QR, NFC), with optional sender approval.
| Layer | Location | Role | | Layer | Path | Responsibility |
|--------|----------|------| |-------|------|----------------|
| Rust core | `crates/vnidrop/` | Iroh networking, blob store, SQLite, approval, tickets, streaming | | Rust core | `crates/vnidrop/` | Iroh endpoint, blobs, SQLite, tickets, approval, streaming |
| Shared UI | `shared/` | Compose Multiplatform UI, ViewModels, platform bridges | | Shared KMP | `shared/` | Compose UI, ViewModels, expect/actual platform bridges |
| Apps | `androidApp/`, `iosApp/`, `desktopApp/` | Thin hosts | | Hosts | `androidApp/`, `iosApp/`, `desktopApp/` | Thin app shells |
| UniFFI | Gobley + `crates/vnidrop/uniffi.toml` | Kotlin bindings to `VnidropCore` |
**Non-negotiable product rule:** platform/UI code opens files and reacts to **Invariant:** UI/platform opens files and handles pickers; **Rust streams bytes**.
events; **Rust owns byte streaming**. Do not pull transfer payloads through Do not design features that move transfer payloads through Kotlin heap by default.
Kotlin heap as the default path.
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) - [`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/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 1. Prefer PRs into `master`. Do not merge to `master` locally unless the user asks.
user explicitly asks. 2. Do not `git push`, force-push, or open a PR unless the user asks.
2. **Do not push, force-push, or open a PR** unless the user asks. 3. If `commit.gpgsign` is enabled, create **signed** commits only. If signing fails
3. **Signed commits** when the repo enables signing (`commit.gpgsign`). If (empty `ssh-add -l`), stop and tell the user to unlock the key. Never switch to
signing fails (empty agent / passphrase), stop and ask the user to unlock unsigned commits to unblock” yourself.
the key (`ssh-add …`). Do not silently create unsigned commits. 4. Change only files required for the task. No drive-by refactors, dependency bumps,
4. **Minimal scope.** Change only what the task requires. No drive-by refactors, or repo-wide formatting.
dependency bumps, or reformatting unrelated files. 5. Do not force architecture migrations (MVI, Hilt, Nav3, etc.) unless requested.
5. **Do not force architecture migrations.** Adapt to existing patterns 6. Never commit secrets, key material, or passphrases.
(ViewModels, feature packages, Rust modules). Structural rewrites only when 7. Destructive git (`reset --hard`, `push --force`, dropping DBs) only with explicit
requested or when fixing a clear violation. user approval.
6. **Never commit secrets**, API keys, keychains, or passphrases. Never paste 8. **Every bug fix includes a regression test** at the lowest layer that catches it.
user passphrases into chat or files. 9. After code changes, run the **relevant** checks in [Build and test](#build-and-test)
7. **Destructive git** (`reset --hard`, `push --force`, dropping data) only with and fix failures before finishing.
explicit user approval.
8. **Bug fixes need tests** at the correct layer (see §6).
--- ---
## 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, ### Rust core (`crates/vnidrop` or workspace root)
navigation, resources, ViewModel↔UI wiring, accessibility, lists, animation),
load and follow: 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 ```text
.codex/skills/compose-skill/SKILL.md .codex/skills/compose-skill/SKILL.md
``` ```
Do **not** invent a parallel Compose style guide. The skill is the source of - Open at most one `references/*.md` file when the skills Quick Routing requires it.
truth for Compose/CMP defaults. - 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 Details: [`shared/AGENTS.md`](shared/AGENTS.md).
1. Read existing feature code first (conventions beat generic tutorials).
2. Apply the skills 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 skills 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.<send\|receive\|settings\|approvals\|app>` + `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-skills “Existing Project Policy” applies: preserve working structure.
--- ---
## 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 Layout:
- 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
| Layer | Location | | Layer | Location |
|--------|----------| |-------|----------|
| Rust unit / crate-private | `crates/vnidrop/src/tests/` | | Rust unit / private | `crates/vnidrop/src/tests/` |
| Rust integration | `crates/vnidrop/tests/` + `tests/support/` | | Rust integration | `crates/vnidrop/tests/` |
| Shared pure logic | `shared/src/commonTest/` | | Shared logic | `shared/src/commonTest/` |
| Shared Compose / JVM | `shared/src/jvmTest/` | | 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
```
--- ---
## 7. Git and PR conventions ## Git and PR instructions
### Branches ### 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` - Good: `feat/folder-share`, `fix/cancel-export-hang`, `docs/agents-md`
- Bad: `feat/step3-remaining`, `temp`, `wip` - Bad: `feat/step3-remaining`, `wip`, `temp`
Delete merged feature branches **locally and on origin** after the PR lands; After a PR merges: delete the feature branch **locally and on `origin`**, then
start the next task from updated `master`. branch from updated `master`.
### Commits ### Commits
- Prefer conventional style used in history: `feat(scope):`, `fix(scope):`, - Style in history: `feat(scope):`, `fix(scope):`, `refactor(scope):`, `docs:`, `ci:`.
`refactor(scope):`, `docs:`, `style:`, `ci:`. - Subject = outcome; body only when needed.
- Subject focuses on **why / user-visible outcome**, complete sentences in the - Signed when repo requires it.
body when needed.
- Keep commits reviewable; do not mix unrelated features.
### Pull requests ### Pull requests
- Title matches the main change. - Title matches the main change.
- Summary: short bullets of **what** and **why**. - Summary: short bullets of what/why.
- **Test plan must be useful for this PR**: - **Test plan must be executable for this PR**:
- Exact commands to run, and/or - exact commands, and/or
- 12 concrete device/UI scenarios that would catch a regression. - 12 concrete scenarios that would catch a regression.
- Avoid generic filler (“CI green”, “test everything”) with no commands or - No filler plans (“everything works”, “CI green”) without commands or scenarios.
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.
--- ---
## 8. Implementation checklist (before finishing a task) ## Security considerations
1. Read surrounding code and this file; for UI, load `compose-skill`. - Treat tickets and endpoint IDs as sensitive enough not to log full blobs in
2. Implement the smallest correct change. production paths.
3. Add/adjust tests for behavior changes and bug fixes. - Do not weaken approval/access checks for convenience.
4. Run the relevant format/lint/test commands. - Do not store secrets in the repo; app data dirs and key files stay out of git.
5. Comments only where they add non-obvious information. - Be careful with file publish races (no-clobber rename/link policy exists for a reason).
6. Commit (signed) if asked; push/PR only if asked.
7. Leave a concise summary of what changed and how it was verified.
--- ---
## 9. Anti-patterns (do not) ## Common tasks → start files
- 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
| Task | Start here | | Task | Start here |
|------|------------| |------|------------|
| Share import / multi-file / folders | `runtime/share.rs`, `filesystem.rs`, platform `FileSystemService.*` | | Share / multi-file / folders | `runtime/share.rs`, `filesystem.rs`, platform `FileSystemService.*` |
| Receive path / sink export | `runtime/receive.rs`, Android/iOS sinks | | Receive / export / sinks | `runtime/receive.rs` |
| Cancel / delete / stop share | `runtime/lifecycle.rs`, `facade.rs` cancel path | | Cancel / delete / stop share | `runtime/lifecycle.rs`, `facade.rs` |
| Per-receiver send progress | `runtime/provider.rs`, `AppUiModels.kt`, `TransferDetails.kt` | | Per-receiver send progress | `runtime/provider.rs`, `ui/state/AppUiModels.kt` |
| Approvals UI | `feature/approvals/`, `approval.rs` | | Approvals | `feature/approvals/`, `approval.rs` |
| Ticket / QR / NFC | `TransferShareActions.*`, `ReceiveInvitationActions.*` | | QR / NFC invitations | `TransferShareActions.*`, `ReceiveInvitationActions.*` |
| Theme / brand color | `ui/theme/VniDropTheme.kt` | | Theme / brand | `ui/theme/VniDropTheme.kt` |
| Compose skill | `.codex/skills/compose-skill/SKILL.md` | | Compose skill | `.codex/skills/compose-skill/SKILL.md` |
--- ---
*Last aligned with postPR #10 tree (`runtime/` split, multi-file/folder share, ## Anti-patterns (never)
iOS QR/NFC, per-receiver progress, cancel-export fixes).*
- 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.

128
crates/vnidrop/AGENTS.md Normal file
View File

@@ -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
```

126
shared/AGENTS.md Normal file
View File

@@ -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 skills 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`