The plugin versions could not be matched upward without this: both
tauri-plugin-log 2.9.0 and tauri-plugin-updater 2.10.1 require tauri
^2.10, and the tree was on 2.9.5. So the framework moves with them --
tauri 2.9.5 -> 2.11.5, tauri-build 2.5.3 -> 2.6.3, wry 0.53.5 -> 0.55.1
-- and every plugin's Rust crate and npm package is now pinned to the
same version on both sides.
That upgrade broke Android outright, and the breakage is the interesting
part.
Seven call sites in this crate reach JNI through
ndk_context::android_context(), which reads a process-global pair of
pointers. Nothing here ever set that global. `tao` did -- the windowing
layer under wry, three levels below anything this project names in
Cargo.toml. tao 0.34.5 called initialize_android_context() while starting
the activity and our code read what it left behind. tao 0.35.3 keeps the
same two pointers in a private struct and no longer publishes them.
The result, on every launch, was:
PANIC at ndk-context/src/lib.rs:72: android context was not initialized
8: ndk_context::android_context
9: jellytau_lib::run::{{closure}}
Not a crash in our code, and not a change to our code: an undocumented
side effect of a transitive dependency disappeared. Relying on someone
else to populate a global is a dependency that does not appear in
Cargo.toml and gives no warning when it goes.
src-tauri/src/android_context.rs now owns that invariant instead of
assuming it. JNI_OnLoad captures the JavaVM as the shared library loads
-- the earliest moment available, and nothing in tao, wry or tauri
defines one to collide with. The Context is resolved lazily via
ActivityThread.currentApplication() and pinned as a global reference for
the process lifetime, since ndk_context stores a bare pointer and does
not own it. It publishes the Application rather than the Activity:
SecureStorage.initialize() immediately reduces its argument to
applicationContext anyway, and an Application cannot outlive itself the
way a retained Activity would.
Restoring the global keeps all seven callers untouched. Threading a VM
and Context handle through five credential call sites would have been a
larger change with more risk, on the credential path.
Failure now degrades instead of aborting: it is logged and credentials
fall back to the encrypted-file path, which the app already supports.
Verified on a device, R8-minified, not merely compiled:
[INIT] Android JavaVM and Application published to ndk_context
Android SecureStorage initialized successfully
Android Keystore available via SecureStorage
[INIT] Using system keyring for credential storage
[CodecDetection] Detected 7 video codecs: av1,h263,h264,hevc,...
-- the real keystore path, not the fallback, and the app stays up. None
of this is reachable by CI: nothing there runs the app.
Also fixed here, both found the same way:
- `tauri android build --apk true` is now `--apk`. The CLI took a value
until 2.10; from 2.11 the stray `true` is a positional and the build
fails before starting. Three call sites in build-android.sh and one
in build-release.yml -- the latter builds the signed APK, by far the
most-downloaded artifact.
- scripts/build-android.sh ran `npm install` on its clean-build path in
a bun project, ignoring bun.lock and re-resolving the tree. That is
exactly how the plugin crate/package versions drift apart again.
scripts/check-tooling.sh now fails on any npm/yarn/pnpm invocation or
foreign lockfile, and runs in CI.
DR-222, DR-223.
Development Scripts
Collection of utility scripts for building, testing, and deploying JellyTau.
Testing Scripts
test-all.sh
Run all tests (frontend + Rust backend).
./scripts/test-all.sh
test-frontend.sh
Run frontend tests only.
./scripts/test-frontend.sh # Single pass (same as `bun run test`)
./scripts/test-frontend.sh --watch # Watch mode
./scripts/test-frontend.sh --ui # Open UI
bun run test is vitest run — one pass, exit code, done. It used to be bare
vitest, which parked in watch mode; CLAUDE.md's "Before Committing" list tells
people to run it, so it had to terminate. The interactive modes moved to their
own entry points:
| Command | Runs |
|---|---|
bun run test |
vitest run — single pass |
bun run test:watch |
vitest — watch mode |
bun run test:ui |
vitest --ui |
bun run test:coverage |
vitest run --coverage |
test-frontend.sh forwards any extra arguments to vitest and switches to the
long-running form automatically when it sees --watch, -w, or --ui.
test-rust.sh
Run Rust tests only.
./scripts/test-rust.sh # Run all tests
./scripts/test-rust.sh -- --nocapture # Show println! output
Android Scripts
build-android.sh
Build the Android APK.
./scripts/build-android.sh # Debug build
./scripts/build-android.sh release # Release build
deploy-android.sh
Install APK on connected Android device.
./scripts/deploy-android.sh # Deploy debug APK
./scripts/deploy-android.sh release # Deploy release APK
build-and-deploy.sh
Build and deploy in one command.
./scripts/build-and-deploy.sh # Build + deploy debug
./scripts/build-and-deploy.sh release # Build + deploy release
check-android.sh
Check Android development environment setup.
./scripts/check-android.sh
logcat.sh
View Android logcat filtered for the app.
./scripts/logcat.sh
Traceability & Documentation
extract-traces.ts
Extract requirement IDs (TRACES) from source code and generate a traceability matrix mapping requirements to implementation locations.
bun run traces # Generate markdown report
bun run traces:json # Generate JSON report
bun run traces:markdown # Save to docs/traceability.md
bun run traces:coverage # Coverage gate — exits non-zero below the ratchet
bun run traces:validate # Dangling-ID gate — every traced ID must be defined
The script scans all TypeScript, Svelte, and Rust files (plus scripts/)
looking for TRACES: comments and generates a comprehensive mapping of:
- Which code files implement which requirements
- Line numbers and code context
- Coverage summary by requirement type (UR, IR, DR, JA)
bun run traces:coverage is the supported way to check requirement coverage
locally — it runs the same computation CI does. Coverage denominators are
derived from docs/requirements.md at run time; they are never hardcoded. An ID
that appears in a TRACES: comment but is not defined in requirements.md is
reported as orphaned and does not count toward coverage (see DR-093).
bun run traces:validate is the dangling-ID gate. It fails if any traced ID
— including UT/IT, which coverage deliberately ignores — is not defined as a
table row in requirements.md, printing each offender with the files that
reference it. Without it the extractor accepted any well-formed ID silently, so
typos and renames that missed a call site went unreported for months.
Removed:
check-req-coverage.sh,check-test-coverage.sh, andfind-req-implementations.shwere deleted in July 2026. They read an undocumented@req:tag convention parallel toTRACES:, greppedsrc-tauri/unscoped (hanging on ~40 GB oftarget/artifacts), and in one case reported "all requirements implemented" from an empty result set.extract-traces.tsis the single source of truth for requirement coverage. See it reportedTotal Requirements: 1and then "All requirements have implementations!". Nothing referenced it. Usebun run traces:coverage.
Example TRACES comment in code:
// TRACES: UR-005, UR-026 | DR-029
function handlePlayback() { ... }
See docs/traceability.md for the latest generated mapping.
CI/CD Validation
The traceability system is integrated with Gitea Actions CI/CD:
- Automatically validates TRACES on every push and pull request
- Enforces a minimum coverage threshold (a ratchet: raise it, never lower it)
- Fails on dangling IDs — traced but undefined in
requirements.md - Warns if new code lacks TRACES comments
- Generates traceability reports automatically
For details, see:
- Traceability CI Guide - Full CI/CD documentation
- TRACES Quick Reference - Quick guide for adding TRACES
Linting & Formatting
There is no script wrapper for these — they are plain package.json entries:
bun run lint # eslint .
bun run lint:fix # eslint . --fix
bun run format # prettier --write .
bun run format:check # prettier --check .
Config lives in eslint.config.js (flat config: typescript-eslint +
eslint-plugin-svelte, tuned for Svelte 5 and TS strict), .prettierrc, and
.prettierignore. src/lib/api/bindings.ts is excluded from both — it is
generated by tauri-specta on every Rust build.
bun run lint is currently error-clean but not warning-clean: several rules
are deliberately set to warn because the existing tree has more hits than a
tooling change should touch (unused bindings, any at the IPC boundary, unkeyed
{#each}). Each one is annotated in eslint.config.js with why, and the
intended end state is error. Drive them down; do not delete them.
no-console is switched off for now — see the note in eslint.config.js.
Git Hooks
install-hooks.sh
Point git at the repo's tracked hooks directory (core.hooksPath).
bun run hooks:install # or: ./scripts/install-hooks.sh
hooks/pre-commit
Runs the fast half of CLAUDE.md's "Before Committing" list so it is enforced rather than remembered:
bun run check(svelte-check)bun run test(vitest, single pass)scripts/check-frontend-boundary.shcargo fmt --all -- --check, only when staged files touchsrc-tauri/
cargo clippy and cargo test are deliberately not in the hook — minutes per
commit is how you teach people to reach for --no-verify. They run in CI, and
locally via bun run test:all.
git commit --no-verify # skip the hook for one commit
git config --unset core.hooksPath # uninstall
The hook skips itself during a merge, rebase, or cherry-pick, and when nothing is staged.
Utility Scripts
clean.sh
Clean all build artifacts.
./scripts/clean.sh
NPM Script Aliases
You can also run these via npm/bun:
bun run test # Frontend tests (single pass)
bun run test:all # All tests
bun run test:rust # Rust tests
bun run lint # ESLint
bun run format:check # Prettier (check only)
bun run hooks:install # Install the git hooks
bun run android:build # Build Android APK
bun run android:deploy # Deploy to device
bun run android:dev # Build + deploy debug
bun run android:check # Check environment
bun run clean # Clean artifacts