Set a CSP with script-src 'self' (Tauri nonces the one inline bootstrap script), object-src/frame-src 'none', and necessarily-permissive img/media/connect for the user-supplied Jellyfin origin. Narrow assetProtocol $APPDATA/** -> thumbnails/**, which is convertFileSrc's only remaining caller. Conflict resolution: scripts/extract-traces.test.ts pinned counts summed rather than side-picked — DR-189 and DR-198 were added independently on two branches, so DR 187 -> 189 and total 330 -> 332. docs/traceability.md regenerated.
470 lines
24 KiB
Markdown
470 lines
24 KiB
Markdown
# JellyTau Codebase Audit
|
|
|
|
**Date:** 2026-08-16 · **Version:** v0.6.0 · **Commit:** `be907b49` (master)
|
|
|
|
A review of the Rust/Svelte/Android codebase against its own requirements matrix
|
|
and against current Android and Tauri v2 platform practice. Every finding was
|
|
verified by running the project's own tooling or reading the code it points at —
|
|
nothing here is inferred from documentation alone.
|
|
|
|
**Scale:** 55,835 LOC Rust · 50,490 LOC TS/Svelte · 530 requirements · 824 traces
|
|
|
|
| Severity | Count |
|
|
|----------|-------|
|
|
| High | 5 |
|
|
| Medium | 10 |
|
|
| Low | 5 |
|
|
| Tests passing | 1,719 |
|
|
| Untraced requirements | 86 |
|
|
| Traceability coverage | 86% (285/330) |
|
|
|
|
> **Revisions, 2026-08-16.** Three rankings changed after device testing and
|
|
> platform research, all documented in place:
|
|
> - **B1 High → Low.** The predicted impact was refuted on a physical Android 16
|
|
> device. The residual risk turned out to be a different, narrower one.
|
|
> - **B7 Low → Medium, re-framed.** The original reading of predictive back was
|
|
> backwards: at targetSdk 36 it is already enabled, not merely un-opted-into.
|
|
> - **B8 added (Medium).** Android 16 Local Network Protections versus a
|
|
> LAN-hosted Jellyfin server.
|
|
>
|
|
> Original ranking was 6 High / 8 Medium / 5 Low.
|
|
|
|
**Verified by running:** `bun run check` · `bun run test` · `cargo test` ·
|
|
`cargo clippy --all-targets` · `bun run check:boundary` · `bun run traces:json`
|
|
|
|
**Device-verified (2026-08-16):** B1 and B2 were checked against a physical HONOR
|
|
ROD2-W09 running Android 16 (SDK 36) with the shipped app installed. B2 was
|
|
confirmed; B1 was refuted and downgraded.
|
|
|
|
**Not covered:** the e2e suite (`test:e2e` is not wired into CI and was not run),
|
|
Windows and Arch packaging paths, and the docs-site build. B3, C1 and C2 still
|
|
need a device/desktop playback pass.
|
|
|
|
---
|
|
|
|
## A. Requirements versus code
|
|
|
|
The traceability matrix is the project's own claim about what is built. Of 530
|
|
defined requirement IDs, 86 carry no `TRACES:` tag anywhere in the tree. Most of
|
|
those gaps are documentation debt rather than missing features — which is
|
|
precisely the problem, because it makes the matrix unreliable as evidence.
|
|
|
|
### A1 · High · Twelve requirements are marked "Done" but have zero traces
|
|
|
|
`UR-006` (lockscreen/BLE control), `UR-037` (video library presentation),
|
|
`IR-006` (Android MediaSession), `IR-008` (audio focus), `IR-022` (person/cast
|
|
API), `IR-024` (home-screen API) and six Jellyfin API requirements (`JA-006`,
|
|
`JA-009`, `JA-013`, `JA-014`, `JA-015`, `JA-018`) all claim completion with
|
|
nothing pointing at an implementation.
|
|
|
|
These features demonstrably work — lockscreen control, Next Up, favourites are
|
|
all shipped. The code is there; the tags are not. That means the matrix currently
|
|
over-reports on exactly the requirements a reviewer would most want to verify,
|
|
and a regression in any of them would leave no trace to follow.
|
|
|
|
**Fix:** Tag the existing implementations. Highest value per keystroke in the
|
|
whole audit: six of the twelve are single Jellyfin API call sites.
|
|
|
|
### A2 · Medium · Requirement statuses contradict each other across layers
|
|
|
|
`UR-020` (subtitle selection) and `UR-021` (audio track selection) are marked
|
|
*Done*, while the integration requirements they decompose into — `IR-018` and
|
|
`IR-019`, both libmpv-specific — are still *Planned*. Similarly `IR-005` (MPRIS)
|
|
sits at *Planned* under a *Done* `UR-006`.
|
|
|
|
The likely truth is that these user requirements were satisfied through a
|
|
different path than the one originally specified (HTML5 `<video>` and ExoPlayer
|
|
rather than libmpv), and the IRs were never re-scoped. Left as-is, the matrix
|
|
reads as though shipped features depend on unbuilt integrations.
|
|
|
|
**Fix:** Re-scope or retire the stale IRs so each Done UR rests on Done IRs.
|
|
|
|
### A3 · Medium · The traceability gate is set far below actual coverage
|
|
|
|
`traceability-check.yml` fails only below 50%. Real coverage is well above that,
|
|
so the gate cannot catch a coverage regression until roughly half the matrix has
|
|
rotted. A gate that can only fire after a catastrophe is not protecting anything.
|
|
|
|
**Measured coverage: 86% (285/330)** — UR 71/75, IR 19/32, DR 166/187, JA 29/36.
|
|
IR is by far the weakest dimension, which corroborates A1.
|
|
|
|
**Fix applied:** `MIN_THRESHOLD` ratcheted 50 → 82, with the ratchet policy
|
|
written into the workflow (only goes up; never lowered to make a red build pass).
|
|
The same figure is mirrored as `MIN_COVERAGE_PERCENT` in
|
|
`scripts/extract-traces.ts` so local `traces:coverage` gates on the same bar, and
|
|
a test parses the workflow YAML and fails if the two drift apart.
|
|
|
|
### A4 · Low · Two traced IDs do not exist in the requirements document
|
|
|
|
`DR-189` and `UT-188` are referenced by `TRACES:` comments but are defined
|
|
nowhere in `docs/requirements.md`. The extraction tool accepts them silently, so
|
|
typos and renames pass unnoticed.
|
|
|
|
**Fix:** Add a dangling-ID check to the extractor and fail CI on it — cheap, and
|
|
it keeps the matrix honest in both directions.
|
|
|
|
### A5 · Not a gap · The remaining untraced requirements are legitimately unbuilt
|
|
|
|
`UR-016`, `UR-022` and `UR-070` are Planned or Proposed, and `UR-031`
|
|
(crossfade) is explicitly blocked by `DR-034`. Their absence from the trace graph
|
|
is correct and needs no action — noted so it does not get swept into the fix list.
|
|
|
|
---
|
|
|
|
## B. Android platform practice
|
|
|
|
The app targets SDK 36 with a minSdk of 24. Several manifest and WebView settings
|
|
still reflect an earlier target level.
|
|
|
|
### B1 · Low · `POST_NOTIFICATIONS` is declared but never requested at runtime
|
|
|
|
*Downgraded from High. The original ranking was refuted by device testing — the
|
|
evidence is below, and it is the reason this finding is now near-trivial.*
|
|
|
|
The permission appears in the manifest, but there is no `requestPermissions` call
|
|
anywhere in the Kotlin, Rust or TypeScript sources, and
|
|
`JellyTauPlaybackService.startForeground()` runs with no `checkSelfPermission`
|
|
guard. On Android 13+ notification permission defaults to denied.
|
|
|
|
This was ranked High on the theory that it would suppress the media notification
|
|
and with it the lockscreen transport controls (`UR-006`). Testing on an HONOR
|
|
ROD2-W09 running **Android 16 (SDK 36)**, with the shipped app installed and
|
|
playing, shows otherwise. The permission is genuinely denied:
|
|
|
|
```
|
|
POST_NOTIFICATIONS: granted=false, flags=[USER_SENSITIVE_WHEN_GRANTED|USER_SENSITIVE_WHEN_DENIED]
|
|
appops POST_NOTIFICATION: ignore
|
|
```
|
|
|
|
and the notification is nonetheless live and complete:
|
|
|
|
```
|
|
ServiceRecord{... com.dtourolle.jellytau/.player.JellyTauPlaybackService}
|
|
isForeground=true foregroundId=1 types=0x00000002
|
|
foregroundNoti=Notification(flags=NO_CLEAR|FOREGROUND_SERVICE
|
|
category=transport actions=3 vis=PUBLIC)
|
|
```
|
|
|
|
Foreground-service notifications are exempt from `POST_NOTIFICATIONS` — a
|
|
foreground service cannot run without one. All three transport actions are
|
|
present. **`UR-006` is not at risk.**
|
|
|
|
What remains is minor. The FGS notification is the only one the app ever posts:
|
|
the single `notificationManager.notify(NOTIFICATION_ID, …)` call updates that same
|
|
foreground notification, so it inherits the exemption while the service is
|
|
foreground. The declared permission therefore currently buys nothing.
|
|
|
|
**The real risk here is not the permission — it is how narrowly the exemption is
|
|
earned.** AOSP's `Notification.isMediaNotification()` grants it only when the
|
|
style is `MediaStyle`/`DecoratedMediaCustomViewStyle` **and**
|
|
`Notification.EXTRA_MEDIA_SESSION` holds a non-null *platform* session token. If
|
|
either is missing while the permission is denied, the notification is **silently
|
|
suppressed** — no exception, no log.
|
|
|
|
JellyTau earns it at two sites, both of which hang it on a null-safe call:
|
|
|
|
```kotlin
|
|
androidx.media.app.NotificationCompat.MediaStyle()
|
|
.setMediaSession(mediaSessionCompat?.sessionToken) // :273 and :466
|
|
```
|
|
|
|
Ordering currently saves it — `mediaSessionCompat` is assigned in `onCreate`
|
|
(:195) and `createBasicNotification()` is only reached from `onStartCommand`
|
|
(:251) — and the device test confirms it works. But it is one reordering away
|
|
from breaking invisibly, and only for users who denied the permission, which is
|
|
a population most developers never test as.
|
|
|
|
**Fix:** Keep the permission declared — download-service FGS notifications are
|
|
*not* covered by the media exemption, and this app has a downloads feature that
|
|
may want them. Comment both `setMediaSession` sites to record what earns the
|
|
exemption, and log loudly if the token is ever null at build time, converting a
|
|
silent failure into a diagnosable one.
|
|
|
|
**Location:** `src-tauri/android/src/main/java/com/dtourolle/jellytau/player/JellyTauPlaybackService.kt:251`, `:273`, `:466`
|
|
|
|
### B2 · High · Cloud backup is on by default, and it will break credential restore
|
|
|
|
The manifest sets neither `android:allowBackup="false"` nor a
|
|
`dataExtractionRules`/`fullBackupContent` file, so Android's default applies: the
|
|
app's data directory is backed up to the user's Google account. That ships the
|
|
SQLite catalogue — library metadata and watch history — off the device.
|
|
|
|
The credential path makes it worse rather than better. `SecureStorage.kt`
|
|
encrypts with AES/GCM under an Android Keystore key, and Keystore keys are never
|
|
backed up. A user restoring onto a new phone therefore gets the ciphertext
|
|
without the key: undecryptable credentials and a silent authentication failure,
|
|
with no code path that recognises the situation.
|
|
|
|
**Fix applied.** `allowBackup="false"`. Extraction rules that merely excluded the
|
|
DB and credential prefs would have left nothing worth backing up: the SQLite
|
|
catalogue is a rebuildable mirror of the server and watch state lives server-side,
|
|
so there is no user-authored data to preserve.
|
|
|
|
**A gap this audit missed:** on API 31+, `allowBackup="false"` disables *cloud*
|
|
backup but **not device-to-device transfer**, which reproduces the identical
|
|
failure — the prefs travel, the Keystore key does not. A
|
|
`data_extraction_rules.xml` excluding all five domains from both `<cloud-backup>`
|
|
and `<device-transfer>` was added to close it.
|
|
|
|
**A real bug found while fixing this:** the Rust encrypted-file fallback in
|
|
`credentials.rs` propagated a decrypt failure as `CredentialError::Encryption`,
|
|
which `storage_get_access_token` turned into a hard `Err` — so an undecryptable
|
|
blob was an error state, not a logout. It now logs and returns an empty map, so
|
|
the caller sees `NotFound` → `Ok(None)` → login screen, and the next sign-in
|
|
self-heals the file. `SecureStorage.getCredential` on the Kotlin side already
|
|
returned null, but could not distinguish "nothing stored" from "unreadable" and
|
|
left the dead blob in prefs forever; it now separates the cases and discards it.
|
|
Three tests written and watched fail first, per the red→green rule.
|
|
|
|
### B3 · High · `MIXED_CONTENT_ALWAYS_ALLOW` undoes the network security config
|
|
|
|
`network_security_config.xml` is careful and well-argued: cleartext blocked
|
|
everywhere, exempted only for `127.0.0.1` so the local media server can serve
|
|
downloads. Its own comment warns "this must not become a blanket cleartext
|
|
opt-in."
|
|
|
|
But `MainActivity.kt` sets `mixedContentMode = MIXED_CONTENT_ALWAYS_ALLOW`, which
|
|
permits the WebView to load http subresources into an https page from any origin.
|
|
Alongside it, `allowFileAccess = true` and `allowContentAccess = true` are both
|
|
broader than anything the app needs, since Tauri serves the UI from its own scheme
|
|
and media comes from the token-guarded loopback server. These read as leftovers
|
|
from before the media server existed.
|
|
|
|
**Fix:** Drop to `MIXED_CONTENT_COMPATIBILITY_MODE` and set both file and content
|
|
access to false, then verify offline video still plays.
|
|
|
|
**Location:** `src-tauri/android/src/main/java/com/dtourolle/jellytau/MainActivity.kt:504-507`
|
|
|
|
### B4 · Medium · Android TV is half-declared
|
|
|
|
The manifest advertises `LEANBACK_LAUNCHER` and a non-required leanback feature,
|
|
but omits `<uses-feature android:name="android.hardware.touchscreen"
|
|
android:required="false"/>` and an `android:banner`. That combination fails Play's
|
|
TV validation, and on a real TV the app would launch into a UI with no D-pad focus
|
|
model behind it.
|
|
|
|
**Fix:** Either commit to TV — add the feature declaration, a banner, and a focus
|
|
pass — or remove the leanback category until you do.
|
|
|
|
### B5 · Medium · `jvmTarget` is pinned to 1.8 under compileSdk 36
|
|
|
|
The Kotlin target has not moved with the SDK. AGP 8 warns on it, and it locks the
|
|
Kotlin sources out of APIs and desugaring behaviour that everything else in the
|
|
toolchain assumes.
|
|
|
|
**Fix:** Move `jvmTarget` and the Java source/target compatibility to 17.
|
|
|
|
### B6 · Low · Media3 is several minor versions behind
|
|
|
|
`androidx.media3` is pinned at 1.5.0 across exoplayer, hls, session and common.
|
|
Given how much of this app's hard-won behaviour lives in ExoPlayer edge cases —
|
|
truncated progressive streams, background audio handoff, HLS resume — staying
|
|
current on its bug-fix releases has unusually high value here.
|
|
|
|
**Fix:** Schedule a Media3 bump with a device pass over the playback regression list.
|
|
|
|
### B7 · Medium · Predictive back is already on, not merely un-opted-into
|
|
|
|
*Upgraded from Low, and re-framed — the original framing was backwards.*
|
|
|
|
The audit first read the absent `enableOnBackInvokedCallback` as the app
|
|
*forgoing* the Android 13+ back-gesture preview. That is not what the flag means
|
|
at this target level. Predictive back is enabled by default for apps targeting
|
|
recent SDKs, and Android 16's own behaviour-change list carries "Migration or
|
|
opt-out required for predictive back" — with the opt-out being removed. Targeting
|
|
36, JellyTau is already getting predictive back; it simply hasn't been checked
|
|
against it.
|
|
|
|
That matters more than a missing opt-in would, because the app does not use
|
|
ordinary Android back. It runs a WebView with its own history model —
|
|
`src/lib/utils/navigation.ts` tracks a depth counter, applies a popstate delta,
|
|
and falls back to a path when `history.back()` would trap the user, with
|
|
`scrollRestore.ts` keying off the same popstate events. That is exactly the kind
|
|
of custom back handling predictive back is most likely to disagree with.
|
|
|
|
**Fix:** This is a device test, not a code change — exercise the back gesture
|
|
(including the drag-and-release preview and the cancel) from a library page, a
|
|
detail page, the player, and the settings screen, and watch for the depth counter
|
|
desynchronising. Only change code if it misbehaves.
|
|
|
|
Separately and unrelatedly: `JellyTauPlaybackService` is `exported="true"` with a
|
|
`MediaSessionService` intent filter — conventional for Media3, but it means any
|
|
app on the device can attempt to bind and drive playback. Confirm the session's
|
|
`onConnect` callback rejects unknown packages.
|
|
|
|
### B8 · Medium (forward-looking) · Android 16 Local Network Protections vs a LAN Jellyfin server
|
|
|
|
*New finding, surfaced while researching B1.*
|
|
|
|
Android 16's behaviour-change list includes **Local Network Permission**. JellyTau's
|
|
entire purpose is reaching a Jellyfin server that, for most users, sits on the
|
|
local network — so a permission gate on local-network access is a direct threat to
|
|
the app's core function, not a peripheral concern.
|
|
|
|
Stated carefully, because the timing matters: in Android 16 this is **opt-in for
|
|
testing**, not enforced by default, with enforcement signalled for a future
|
|
release. Nothing is broken today, and the device test will not surface it. But
|
|
this is the rare platform change that could stop the app working at all, and it
|
|
is much cheaper to handle before it is mandatory.
|
|
|
|
**Fix:** Investigate what the permission will require, then test the app against
|
|
it with the opt-in flag enabled on the Android 16 device already to hand. Track it
|
|
as a release-blocking item for whichever Android version enforces it.
|
|
|
|
---
|
|
|
|
## C. Tauri v2 configuration
|
|
|
|
The capability model here is genuinely well done — see section E. The gaps are in
|
|
the two settings that govern what a compromised web layer could reach.
|
|
|
|
### C1 · High · `"csp": null` contradicts the project's own security convention
|
|
|
|
`CLAUDE.md` lists "keep the CSP restrictive in `tauri.conf.json`" as a standing
|
|
rule; the config disables CSP entirely. With it off, any script that reaches the
|
|
web layer inherits the full IPC surface.
|
|
|
|
The realistic exposure today is low, and worth stating plainly rather than
|
|
inflating: the frontend has a single `{@html}` — an app-owned icon in
|
|
`GenericGenreBrowser.svelte`, not server data — and no `innerHTML`, `eval` or
|
|
`new Function` outside tests. So this is a missing defence rather than an open
|
|
hole. But it is the defence that stops the next careless interpolation of a
|
|
Jellyfin-supplied string from becoming a full compromise.
|
|
|
|
**Fix:** Set a CSP permitting `'self'`, `asset.localhost`, `http://127.0.0.1:*`
|
|
for media, and the configured Jellyfin origin for images. Expect one or two
|
|
iterations against HLS playback.
|
|
|
|
### C2 · Medium · The asset protocol scope is wider than what it serves
|
|
|
|
`assetProtocol.scope` is `$APPDATA/**`, which covers the whole app data directory
|
|
— the SQLite database and the credential store included — while the protocol only
|
|
needs to reach cached thumbnails and downloaded media.
|
|
|
|
Since `DR-137` introduced the token-guarded loopback media server, the asset
|
|
protocol's remaining job may be thumbnails alone, which would make the narrowing
|
|
nearly free.
|
|
|
|
**Fix:** Scope it to the thumbnail and download subdirectories, and confirm
|
|
nothing else still resolves through `convertFileSrc`.
|
|
|
|
### C3 · Low · Shipped desktop bundles have no update path
|
|
|
|
The bundle targets deb, rpm and nsis, but `tauri-plugin-updater` is not among the
|
|
dependencies. Every desktop user upgrades by manually fetching a new package,
|
|
which in practice means a long tail of installs pinned to whatever version they
|
|
first downloaded.
|
|
|
|
**Fix:** Add the updater plugin with a signed release manifest, or document the
|
|
manual upgrade path in the README so the omission is at least deliberate.
|
|
|
|
---
|
|
|
|
## D. CI and code health
|
|
|
|
Local discipline in this project is strong and well documented. CI enforces only
|
|
part of it, which means the discipline holds exactly as long as every contributor
|
|
remembers it.
|
|
|
|
### D1 · High · CI runs neither `cargo clippy` nor `cargo fmt --check`
|
|
|
|
`CLAUDE.md` requires both before committing. Neither appears anywhere in
|
|
`.gitea/workflows/`. The build-and-test job runs the boundary check, the frontend
|
|
tests, the Rust tests and an Android `cargo check` — a good set, with the two lint
|
|
gates missing.
|
|
|
|
Clippy currently reports 51 warnings across the lib and its tests, including
|
|
unused imports and a redundant import that a gate would have stopped at the door.
|
|
|
|
**Fix:** Add both to the test job. Start with `-D warnings` on new code only if
|
|
clearing the existing 51 is too large a first step.
|
|
|
|
### D2 · Medium · A flaky test will intermittently redden CI
|
|
|
|
`offlineCatalog.test.ts` — "pushes include=true while the server is reachable"
|
|
(`UT-068`) — timed out at the 5 s limit during a full-suite run, then passed twice
|
|
in isolation taking 1.13 s and 0.61 s.
|
|
|
|
**Root cause (corrected):** this audit originally attributed it to a real
|
|
wall-clock timer. It isn't. The cost is the **first dynamic
|
|
`import("./offlineCatalog")`**, which pays to transform the service and its whole
|
|
dependency graph (~1072 ms cold) inside a test body, charged against vitest's 5 s
|
|
default. Later re-imports after `vi.resetModules()` cost ~30 ms. Under full-suite
|
|
contention the cold transform alone crosses the limit.
|
|
|
|
**Fix applied:** warm the import once at collection time with a top-level
|
|
`await import(...)`, so no test is timing the compiler. Slowest test 1072 ms →
|
|
129 ms; file total 1170 ms → 238 ms. Timeout deliberately left at the default.
|
|
A latent cross-test leak was also fixed alongside it — the store shim's
|
|
subscribers were never cleared, so every module instance discarded by
|
|
`resetModules()` kept pushing its own visibility value.
|
|
|
|
**Location:** `src/lib/services/offlineCatalog.test.ts:58`
|
|
|
|
### D3 · Medium · 820 `unwrap()`/`expect()` calls sit outside test code
|
|
|
|
They cluster in exactly the files that have historically produced the worst bugs:
|
|
`player/mod.rs` (145), `repository/offline.rs` (125), `storage/mod.rs` (70),
|
|
`commands/download/mod.rs` (51). A panic inside a Tauri command kills the task and
|
|
can leave shared player state inconsistent.
|
|
|
|
Related: 33 raw `.lock().unwrap()` / `.read().unwrap()` / `.write().unwrap()`
|
|
calls remain despite the project's own `MutexSafe`/`RwLockSafe` convention, so
|
|
poison recovery is not uniform.
|
|
|
|
**Fix:** Sweep the command-handler paths first, since those are the ones with a
|
|
`Result<T, String>` to return into. Convert the 33 raw locks to the safe helpers
|
|
as a mechanical pass.
|
|
|
|
### D4 · Low · Five files carry a disproportionate share of the complexity
|
|
|
|
`player/mod.rs` (4,726 lines), `repository/offline.rs` (4,696),
|
|
`repository/online.rs` (3,702), `commands/player/mod.rs` (3,299) and
|
|
`commands/download/mod.rs` (3,226), plus `VideoPlayer.svelte` (2,778) on the
|
|
frontend.
|
|
|
|
These are the same files the changelog keeps returning to for deadlocks and
|
|
playback regressions. Not a defect in itself, and not worth a speculative
|
|
refactor — but the next time one of them needs substantial work, splitting it is
|
|
likely cheaper than continuing to grow it.
|
|
|
|
---
|
|
|
|
## E. Verified sound
|
|
|
|
Things this audit specifically went looking for and found in good order —
|
|
including one that looked alarming from the warning output and turned out to be
|
|
fine.
|
|
|
|
| Area | Finding |
|
|
|------|---------|
|
|
| **The 9 "MutexGuard across await" warnings are test-only** | All nine sit in `#[tokio::test]` functions holding a serialization lock, not in the production async paths that `CLAUDE.md`'s deadlock gotcha warns about. |
|
|
| **The local media server is exemplary** | Loopback-only bind, a 32-hex-char per-session token, lexical `..` folding rather than `canonicalize`, and a test asserting reads stay inside the data directory. |
|
|
| **Tauri capabilities are minimal** | Three permissions total — `core:default`, `opener:default`, `core:path:default`. No blanket grants, no `withGlobalTauri`. |
|
|
| **SQL is parameterised** | Two `format!`-built statements in the whole Rust tree, neither interpolating caller-controlled input into a query. |
|
|
| **R8 keep rules are correct and explained** | JNI-loaded player and security classes, the JavascriptInterface bridges and Media3 are all kept, each with a comment naming the crash it prevents. |
|
|
| **Type and boundary gates are green** | `svelte-check`: 0 errors, 0 warnings. `check:boundary` passes with three reviewed allowlist entries. 698 Rust tests and 1,021 frontend tests pass. |
|
|
|
|
---
|
|
|
|
## F. Suggested order
|
|
|
|
Sequenced so the cheap gates land before the work they would have caught. B2
|
|
leads because it is the finding a user is most likely to actually feel.
|
|
|
|
*(B1 originally led this list. It was demoted to row 10 after device testing —
|
|
see B1. This is a good advertisement for testing a finding before scheduling
|
|
work against it.)*
|
|
|
|
| # | Finding | What it buys | Effort |
|
|
|---|---------|--------------|--------|
|
|
| 1 | B2 | Catalogue and credentials stop leaving the device; restore stops failing silently | S — **confirmed on device**: `ALLOW_BACKUP` set, Google transport active |
|
|
| 3 | D1 | Lint discipline becomes enforced rather than remembered | S |
|
|
| 4 | B3 | The network security config actually holds | S — needs an offline-playback check |
|
|
| 5 | A1 | The matrix stops over-reporting on twelve shipped requirements | M — mostly mechanical |
|
|
| 6 | D2 | CI stops flaking | S |
|
|
| 7 | C1 · C2 | The web layer stops being one interpolation away from full IPC | M — iterate against HLS |
|
|
| 8 | A2 · A3 · A4 | The matrix becomes self-consistent and defended by a real gate | M |
|
|
| 9 | B4 · B5 · B6 · B7 | Platform hygiene brought level with the SDK target | M |
|
|
| 10 | B1 · D3 · C3 · D4 | Long-tail robustness; opportunistic rather than scheduled | L |
|