chore: Claude Code tooling, Konsist commonMain boundary guard, and R8 keep-rule cleanup (#5859)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
James Rich
2026-06-18 19:01:48 -05:00
committed by GitHub
parent 0ab7a4d8c2
commit 18d402bdd2
12 changed files with 414 additions and 12 deletions

View File

@@ -0,0 +1,50 @@
---
name: crash-investigator
description: Investigates a Firebase Crashlytics issue end-to-end for Meshtastic-Android and returns a tight, distilled verdict. Pulls the issue + events via the Firebase MCP, maps the affected versionCode(s) to git tag/commit/Play track, locates the suspect code from the stack frames, and reports root-cause hypothesis + fix area — WITHOUT dumping raw stack traces into the caller's context. Use when given a Crashlytics issue id/URL, a crash signature, or a "is build NNNN still crashing?" question.
tools: mcp__firebase__crashlytics_list_events, mcp__firebase__crashlytics_batch_get_events, mcp__firebase__crashlytics_get_issue, mcp__firebase__crashlytics_get_report, mcp__firebase__crashlytics_list_notes, Bash, Read, Grep, Glob
model: sonnet
---
You are a crash-triage specialist for the **Meshtastic-Android** KMP app. You investigate one Crashlytics issue and return a compact verdict. Your entire value is doing the noisy parts — pulling events, reading stack traces, mapping build numbers — in your own context and returning only the distilled signal. You are READ-ONLY: never edit code; propose the fix area, don't apply it.
## Inputs you may get
A Crashlytics issue id or console URL, a crash signature / exception class, an affected `versionCode` (build number), or a question like "is 29321034 still affected?". If the issue id is ambiguous, pull a short candidate list first and state which you picked.
## Procedure
1. **Pull the issue + events** with the `mcp__firebase__crashlytics_*` tools: `get_issue` for the summary, `list_events`/`batch_get_events` for representative stack traces, affected versions, device/OS/state breakdown, and event volume over time. Read `list_notes` for prior triage. Use `get_report` for aggregate trends when a time-series matters.
2. **Map versionCode → tag / commit / Play track.** This is fiddly; follow the repo's recipe and NEVER hand-arithmetic a build number into a commit:
- Prefer `gh release list` / `gh release view` — release names embed the versionCode. Match the affected `versionCode` to its release, then read the tag and target commit.
- Fallback: scan git tags and use a tag-count approach; distinct commits can share rev-list counts, so corroborate against the `gh release` name before trusting it.
- Determine the Play track (internal / closed / open / production) from the tag channel suffix (e.g. `-internal.N`, `-closed.N`, production).
- Establish whether the **latest shipped production build** is affected, vs. only older un-updated installs — this is the single most important question for prioritization. "N events but 0 on the current prod build" usually means it's already fixed and the residual is stale installs.
3. **Locate the suspect code.** From the top app frames in the stack (ignore framework/SDK frames), use `Grep`/`Glob`/`Read` to find the file:line. Note the KMP source set (commonMain vs androidMain) and the owning module. If frames point into a library (ktor, maps, kable, MQTT client), say so — the fix may live in a sibling repo (e.g. MQTTastic-Client-KMP) rather than this one.
4. **Form a root-cause hypothesis.** Tie the exception + frames + device/OS/state breakdown together. Note correlations the breakdown reveals (specific OEM, Android version, foreground/background, reconnect storm, etc.).
5. **Repro hint.** If the path is reproducible, point at the mechanism — e.g. the `burningmesh-replay` packet-replay sandbox for radio/packet paths, or the specific user action. Don't actually run it.
## What to return (and ONLY this)
A compact report, no preamble:
```
ISSUE: <id> — <exception class @ top app frame>
STATUS: <NEW / REGRESSION / KNOWN / LIKELY-ALREADY-FIXED> + one-line why
AFFECTED BUILDS: <versionCode(s)> -> <tag(s)> / <commit short shas> / <Play track>
LATEST PROD AFFECTED? <yes/no — build NNNN; this drives priority>
VOLUME: <events / users over the window; trend up/flat/down>
SUSPECT: <module>/<path:line> (<commonMain|androidMain>) [or: library frame -> <which repo>]
ROOT CAUSE (hypothesis): <2-4 lines tying exception + frames + device/state breakdown together>
CORRELATIONS: <only if the breakdown shows one — OEM / OS / state>
REPRO: <mechanism, or "not obviously reproducible">
SUGGESTED FIX AREA: <where a fix would go; do NOT write it>
NOTES: <prior notes, related issues, cross-repo ownership, uncertainty>
```
Rules:
- NEVER paste full stack traces, event JSON, or long Crashlytics payloads. Quote at most the few frames that pin the location.
- Be faithful about uncertainty: if you couldn't confirm the versionCode→commit mapping, say so rather than guessing.
- If the data shows the latest prod build is clean, lead with that — it changes everything downstream.
- Privacy: never surface user identifiers, locations, or key material from event payloads.

View File

@@ -8,11 +8,11 @@ model: haiku
You run Gradle commands for the Meshtastic-Android KMP project and report back a tight, structured result. Your entire value is keeping huge build logs out of the calling agent's context — so you read the full output, but you return only the distilled signal.
## Setup (always, before any Gradle command)
`ANDROID_HOME` is usually unset. Export it first, in the same command line:
**Run from the repository root for THIS session — in a git worktree that is the worktree, NOT the main checkout. Never hardcode a repo path; resolve it.** If the caller's prompt names a specific project/worktree path, `cd` into that; otherwise use the git top-level of your current directory. `ANDROID_HOME` is usually unset. Combine it on one line, and `pwd` so the caller can confirm the right tree was built:
```bash
export ANDROID_HOME="${ANDROID_HOME:-$HOME/Library/Android/sdk}" && ./gradlew <tasks>
cd "$(git rev-parse --show-toplevel)" && pwd && export ANDROID_HOME="${ANDROID_HOME:-$HOME/Library/Android/sdk}" && ./gradlew <tasks>
```
Run from the project root (`/Users/james/StudioProjects/Meshtastic-Android`). Do not `cd` mid-command.
If a build complains `local.properties` is missing (Google-flavor tasks), `cp secrets.defaults.properties local.properties` first — it's git-ignored. Do not `cd` elsewhere mid-command.
## How to run
- Run exactly the task(s) the caller specified. Do not add `clean` unless asked.
@@ -25,6 +25,7 @@ A compact report, no preamble:
```
RESULT: PASS | FAIL | CONFIG-ERROR
DIR: <repo root you actually ran in — flag it if this is a worktree session and the path is the main checkout>
COMMAND: <the gradle task(s) you ran>
<if FAIL — for each failure:>
- <module>:<TestClass>.<method> — <one-line reason / exception type + message>

71
.claude/hooks/post-edit.sh Executable file
View File

@@ -0,0 +1,71 @@
#!/usr/bin/env bash
#
# PostToolUse hook (Edit|Write|MultiEdit) for Meshtastic-Android.
#
# Front-runs three of this repo's own CI/governance gates locally, so the
# failure surfaces at edit time instead of in CI. Dispatches by edited path:
#
# - base strings.xml -> run scripts/sort-strings.py (keeps the file sorted
# and regenerates .skills/compose-ui/strings-index.txt;
# AGENTS.md mandates this but no CI job enforces it)
# - fastlane/metadata/** -> run scripts/check-metadata-length.py and BLOCK on
# overlength store listings (the pull-request.yml
# check-metadata job is blocking; F-Droid #4262)
# - settings.gradle.kts -> remind about the pull-request.yml paths-filter drift
# guard for NEW top-level modules (#5735)
#
# FAILS OPEN: any tooling/parse error allows the edit to stand (exit 0). Notes are
# surfaced to Claude via PostToolUse additionalContext; only the metadata length
# check blocks (exit 2), because that one is a hard CI gate.
input=$(cat)
# jq parses the hook payload; without it, fail open.
command -v jq >/dev/null 2>&1 || exit 0
file_path=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
[ -z "$file_path" ] && exit 0
cwd=$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)
[ -z "$cwd" ] && cwd="$PWD"
repo_root=$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null) || exit 0
[ -n "$repo_root" ] || exit 0
# Emit a non-blocking note back to Claude, then allow the edit.
emit_context() {
jq -n --arg c "$1" \
'{hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:$c}}'
exit 0
}
case "$file_path" in
*core/resources/src/commonMain/composeResources/values/strings.xml)
out=$( (cd "$repo_root" && python3 scripts/sort-strings.py) 2>&1 )
if [ $? -eq 0 ]; then
emit_context "Auto-ran scripts/sort-strings.py: base strings.xml re-sorted and .skills/compose-ui/strings-index.txt regenerated. Line positions changed — re-read the file before any further edits to it."
else
emit_context "Tried to auto-run scripts/sort-strings.py after your strings.xml edit but it failed (likely malformed XML in what was just written — please check):
$out"
fi
;;
*fastlane/metadata/android/*)
out=$( (cd "$repo_root" && python3 scripts/check-metadata-length.py) 2>&1 )
if [ $? -ne 0 ]; then
{
printf '%s\n' "Store-listing metadata exceeds a length limit (scripts/check-metadata-length.py)."
printf '%s\n' "Fix this before it lands — the pull-request.yml check-metadata job is blocking (F-Droid #4262; limits count Unicode code points, not bytes). Details:"
printf '%s\n' "$out"
} >&2
exit 2
fi
exit 0
;;
*settings.gradle.kts)
emit_context "You edited settings.gradle.kts. If you added a NEW TOP-LEVEL module directory, add its '<root>/**' line to the 'android:' paths-filter in .github/workflows/pull-request.yml (case-sensitive) or the verify-check-changes-filter drift guard will fail the PR (bit us on #5735). New sub-modules under an already-listed root (core/**, feature/**, etc.) are already covered — no change needed."
;;
esac
exit 0

73
.claude/hooks/pre-bash-guard.sh Executable file
View File

@@ -0,0 +1,73 @@
#!/usr/bin/env bash
#
# PreToolUse hook (Bash) for Meshtastic-Android. Two jobs:
#
# 1. COMMIT-TIME FORMAT: before a `git commit`, auto-format the STAGED Kotlin
# files with spotlessApply and re-stage them, so committed code always passes
# the blocking spotlessCheck in CI. Directly addresses the recurring
# "forgot to run spotless -> CI fail" loss.
# - Re-stages ONLY the files already staged; pre-existing format fixes in
# other files are left unstaged (visible in `git status`), never silently
# committed. (If the command itself does `git add -A`, those get picked up
# by the command, not by this hook.)
# - Fails open: a gradle/tooling hiccup warns and ALLOWS the commit.
#
# 2. DESTRUCTIVE-OP CONFIRM: surface a confirmation (permissionDecision "ask")
# before an irreversible git op — force-push or `reset --hard`. Flag-order
# robust, unlike a settings.json prefix pattern. Asks, never hard-denies.
#
# FAILS OPEN throughout: missing jq / parse errors / non-git commands -> exit 0.
input=$(cat)
command -v jq >/dev/null 2>&1 || exit 0
cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)
[ -z "$cmd" ] && exit 0
ask() { # $1 = reason; prompt the user to confirm
jq -n --arg r "$1" \
'{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"ask",permissionDecisionReason:$r}}'
exit 0
}
# --- 2. Destructive-op confirmation (cheap checks first) --------------------
if printf '%s' "$cmd" | grep -q 'git push' \
&& printf '%s' "$cmd" | grep -Eq -- '(--force([^-]|$)|[[:space:]]-f([[:space:]]|$))'; then
ask "Force-push detected. This can overwrite remote history irreversibly. Confirm you intend to force-push (consider --force-with-lease instead). Flagged by .claude/hooks/pre-bash-guard.sh"
fi
if printf '%s' "$cmd" | grep -Eq 'git[[:space:]]+reset[[:space:]]+--hard'; then
ask "'git reset --hard' discards uncommitted work irreversibly. Confirm. Flagged by .claude/hooks/pre-bash-guard.sh"
fi
# --- 1. Commit-time spotlessApply on staged Kotlin --------------------------
case "$cmd" in
*"git commit"*) : ;;
*) exit 0 ;;
esac
cwd=$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)
[ -z "$cwd" ] && cwd="$PWD"
repo_root=$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null) || exit 0
[ -n "$repo_root" ] || exit 0
# Staged Kotlin files (added/copied/modified/renamed). Nothing staged -> no-op.
staged=$(git -C "$repo_root" diff --cached --name-only --diff-filter=ACMR -- '*.kt' '*.kts' 2>/dev/null)
[ -z "$staged" ] && exit 0
export ANDROID_HOME="${ANDROID_HOME:-$HOME/Library/Android/sdk}"
out=$( (cd "$repo_root" && ./gradlew spotlessApply --console=plain -q) 2>&1 )
if [ $? -ne 0 ]; then
{
printf '%s\n' "spotless-precommit: spotlessApply failed — allowing the commit anyway (fail-open)."
printf '%s\n' "Run the baseline check before pushing. First lines of output:"
printf '%s\n' "$out" | head -n 20
} >&2
exit 0
fi
# Re-stage only the originally-staged Kotlin files (preserve staging intent).
while IFS= read -r f; do
[ -n "$f" ] && git -C "$repo_root" add -- "$f" 2>/dev/null
done <<< "$staged"
exit 0

View File

@@ -1,5 +1,5 @@
{
"$comment": "Team-wide Claude Code token guard. Claude does NOT read .aiexclude/.copilotignore, so the big-file protections live here. 'deny' = never read (Crowdin-managed translations); 'ask' = prompt first (big files rarely needed in full — prefer .skills/compose-ui/strings-index.txt for strings).",
"$comment": "Team-wide Claude Code config. (1) Read guards: Claude does NOT read .aiexclude/.copilotignore, so the big-file protections live here. 'deny' = never read (Crowdin-managed translations); 'ask' = prompt first (big files rarely needed in full — prefer .skills/compose-ui/strings-index.txt for strings; signing keys should never be read into context). (2) Hooks front-run this repo's own CI gates locally — see .claude/hooks/*.sh for what each does. The pre-bash-guard spotlessApply-on-commit hook is the heaviest (runs gradle at commit time); remove its PreToolUse entry if it's too eager for your workflow.",
"permissions": {
"deny": [
"Read(**/composeResources/**/values-*/*.xml)",
@@ -9,7 +9,37 @@
"Read(**/composeResources/**/values/strings.xml)",
"Read(**/firmware_releases.json)",
"Read(**/composeResources/files/emoji-data.json)",
"Read(**/flatpak-sources*.json)"
"Read(**/flatpak-sources*.json)",
"Read(**/*.keystore)",
"Read(**/*.jks)"
]
},
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/post-edit.sh\"",
"timeout": 60,
"statusMessage": "Post-edit checks (strings sort / metadata length / module CI filter)"
}
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/pre-bash-guard.sh\"",
"timeout": 300,
"statusMessage": "Pre-commit spotlessApply on staged Kotlin / destructive-git confirm"
}
]
}
]
}
}

View File

@@ -0,0 +1,88 @@
---
name: proto-bump
description: Change the org.meshtastic:protobufs pin for Meshtastic-Android — either bump to a tagged release (mergeable) or track develop-SNAPSHOT for a preview (draft), adding/removing the transitive resolution-force hack as appropriate. Verifies with test/allTests (not just compile) via the gradle-runner subagent, audits the new field surface, and opens a PR. Use to consume new proto changes or to re-pin a SNAPSHOT draft onto a tag.
disable-model-invocation: true
---
# proto-bump
Changes the upstream Meshtastic protobufs Maven pin. Protobuf models are **not** generated in this repo — they come from `org.meshtastic:protobufs` (Square Wirebuilt KMP models), pinned in `gradle/libs.versions.toml`. A bump is a catalog edit + verification, never a hand-edit of generated proto.
> **Renovate already watches the catalog** for new *tagged* `org.meshtastic:protobufs` releases and opens the bump PR for you. Use this skill to **drive/finish** such a bump (verify, audit, adapt call sites), or — the part Renovate can't do — to track an unreleased `develop-SNAPSHOT`.
## Two modes
| | Mode A — tagged release | Mode B — develop-SNAPSHOT |
|---|---|---|
| Version | `X.Y.Z` | `develop-SNAPSHOT` |
| Transitive force block | **absent** (remove if present) | **present** (add it) |
| PR state | mergeable | **draft** (un-block when a tag ships) |
| When | a release carries what you need | the change only exists on protobufs `develop` (precedent: #5790, #5834, lockdown re-port) |
Pick the mode from the argument / context. If the change you need isn't in any tag yet, it's Mode B.
## Mode A — bump to a tagged release (mergeable)
1. Read the current pin: `meshtastic-protobufs = "<…>"` in `gradle/libs.versions.toml`. Confirm the target is a real tag at https://github.com/meshtastic/protobufs/releases (or Maven Central), not a SNAPSHOT.
2. Set `meshtastic-protobufs = "X.Y.Z"`.
3. **Re-pin cleanup:** if the transitive force block (see below) is present in the root `build.gradle.kts`, **remove it** — a tagged protobufs is ordered correctly against transitive pins, so the force is unnecessary and misleading once on a release. Also re-check: is takpacket/mqtt now republished against this protobufs? If still pinning an older one, you may need to keep the force (note it in the PR).
4. **Verify** (step "Verification" below) — including `test`/`allTests`.
5. **Audit** the new additive surface (below).
6. Open a normal (non-draft) PR.
## Mode B — track develop-SNAPSHOT (draft only)
1. Set `meshtastic-protobufs = "develop-SNAPSHOT"`.
2. **Add the transitive force block** to the bottom of the root `build.gradle.kts` (exact text below). No repository change is needed — `settings.gradle.kts` already declares the Sonatype maven-snapshots repo (`snapshotsOnly()`) and JitPack (`https://jitpack.io`), which is where `develop-SNAPSHOT` resolves from.
3. **Verify** — including `test`/`allTests` (this is the mode where skipping them bites; see below).
4. **Audit** the new additive surface.
5. Open the PR **as a draft**, stating the un-block condition: *"merge once protobufs `vX.Y.Z` is tagged; switch to Mode A (set the tag + remove the force block) first."*
## The transitive force block (why it exists, exact code)
`takpacket-sdk` (and the MQTT client) transitively pin a **tagged** `protobufs` (e.g. `2.7.25`). Gradle ranks `2.7.25` **above** `develop-SNAPSHOT` (a numeric component outranks the `develop` string qualifier), so the *test runtime* classpath silently downgrades to the tagged proto while the *common-metadata compile* uses the snapshot. The mismatch throws `NoSuchFieldError`/`NoSuchMethodError` on proto-generated classes **at test runtime** — and crucially `assembleDebug`/`detekt` do **not** catch it; only `test`/`allTests` do. The block forces every `org.meshtastic:protobufs*` variant to the snapshot so compile and runtime agree:
```kotlin
// ─── TEMPORARY: protobufs develop-SNAPSHOT preview (PR #NNNN) ─────────────────────────────────────
// We track the unreleased protobufs develop-SNAPSHOT. takpacket-sdk-jvm transitively pins a tagged
// protobufs, and Gradle ranks the tag > develop-SNAPSHOT (a numeric part outranks the "develop"
// string qualifier). That downgrades the test *runtime* classpath to the tag while the common-metadata
// *compile* uses the snapshot, yielding NoSuchFieldError/NoSuchMethodError on the proto-generated
// classes at test runtime (assembleDebug/detekt don't catch it; test/allTests do). Force every
// protobufs* variant to the snapshot so compile and runtime agree. Safe while atak.proto is unchanged,
// so takpacket's own message ABI stays compatible with the newer protobufs.
// REMOVE once protobufs is tagged (Mode A) / takpacket + mqtt are republished against it.
allprojects {
configurations.all {
resolutionStrategy.eachDependency {
if (requested.group == "org.meshtastic" && requested.name.startsWith("protobufs")) {
useVersion("develop-SNAPSHOT")
because("preview #NNNN: override takpacket transitive protobufs pin")
}
}
}
}
```
Update `#NNNN` to the current PR. Remove this entire block when returning to a tagged release (Mode A step 3).
## Verification (don't skip test/allTests)
Dispatch the **gradle-runner** subagent (keep the heavy log out of context). The downgrade failure mode above is a *runtime* classpath problem invisible to compilation, so verification MUST exercise tests:
- `kmpSmokeCompile` + a broad compile of proto-consuming modules (`core:*`, `feature:*`) — catches *compile-time* breaking changes (renamed/removed fields, changed oneof shapes).
- **`test` and `allTests`** — the only gate that catches the transitive runtime downgrade. Treat a green compile as necessary-but-not-sufficient.
Triage each compile/test failure: adapt this repo's call sites, or, if a break is unexpected, stop and report rather than papering over it.
## Audit the new additive surface (recommended)
New proto versions usually add fields/messages the app doesn't consume yet. Diff the new surface against current usage and list what became implementable. **Caveat from prior audits: verify each reference directly** — automated gap-lists for this repo have been wrong as often as right. Treat the list as candidates, not facts; don't implement them in this PR unless asked.
## PR + guardrails
- Write the PR per `.github/copilot-pull-request-instructions.md`: WHY-first; 🛠️ (or 🌟 if it unlocks a user-facing feature); link the upstream release notes (real URL only); "Testing Performed" = the gradle-runner run including `allTests`.
- Never hand-edit or vendor generated proto — this repo consumes the Maven artifact only.
- Keep the change minimal: a bump PR is a catalog edit + the force block (Mode B) + necessary call-site adaptations, not a feature.
- Branch off `main` (the 2.8.0 line) unless told otherwise.
- After a successful change, update the relevant memory pointer (protobufs-sdk-alignment / lora-region-preset-map) so the next session knows the new baseline.

4
.gitignore vendored
View File

@@ -86,10 +86,12 @@ docs/_config_local.yml
flatpak-sources-*.json
flatpak-sources.json
offline-repository/
# Share team-wide Claude config (token-guard deny rules + subagents); keep per-user local settings ignored
# Share team-wide Claude config (settings, subagents, hooks, skills); keep per-user local settings ignored
.claude/*
!.claude/settings.json
!.claude/agents/
!.claude/hooks/
!.claude/skills/
build-scan-*.scan
# burningmesh capture replay assets (private data — generated via replay_server.py --export)
*.fromradio

View File

@@ -0,0 +1,38 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// Architecture-guard module. Holds no production code — only Konsist tests that
// scan every module's source from disk and assert the repo's KMP boundary rules.
// Konsist is JVM-only, so its tests live in jvmTest (it cannot go in commonTest).
// Runs under the existing `allTests` baseline gate via :core:konsist:allTests.
plugins {
alias(libs.plugins.meshtastic.kmp.library)
id("meshtastic.kmp.jvm.android")
}
kotlin {
android { withHostTest {} }
sourceSets {
val jvmTest by getting {
dependencies {
implementation(libs.konsist)
implementation(libs.junit)
}
}
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.core.konsist
import com.lemonappdev.konsist.api.Konsist
import com.lemonappdev.konsist.api.verify.assertFalse
import org.junit.Test
/**
* Enforces the KMP framework-bleed rule from AGENTS.md: shared code in any `commonMain` source set must never depend on
* the JVM (`java.*`) or Android (`android.*`) platform APIs — use the KMP equivalents (Okio, kotlinx-datetime,
* atomicfu, Mutex, …) instead.
*
* Konsist scans every module's Kotlin source from disk, so this single test covers all 37 modules. It runs on the JVM
* (Konsist is JVM-only) under the existing `allTests` baseline gate.
*/
class CommonMainFrameworkBoundaryTest {
private val commonMainFiles = Konsist.scopeFromProject().files.filter { "/src/commonMain/" in it.path }
@Test
fun `commonMain declares no android imports`() {
commonMainFiles.assertFalse { file -> file.imports.any { it.name.startsWith("android.") } }
}
@Test
fun `commonMain declares no java imports`() {
commonMainFiles.assertFalse { file -> file.imports.any { it.name.startsWith("java.") } }
}
}

View File

@@ -1,9 +1,12 @@
# Car App Library ProGuard/R8 rules
# CarAppService must not be obfuscated (resolved by android:exported="true" in manifest,
# but keep rule ensures R8 doesn't remove it during aggressive shrinking)
-keep class org.meshtastic.feature.car.service.MeshtasticCarAppService { *; }
# The service class itself is kept by android:exported="true" in the manifest, and
# obfuscation is off project-wide. The Car App Library constructs it reflectively, so
# pin only the no-arg <init> (its other members are framework overrides, kept as such).
-keepclassmembers class org.meshtastic.feature.car.service.MeshtasticCarAppService {
<init>();
}
# Keep Koin-annotated classes for runtime DI resolution
-keep @org.koin.core.annotation.Single class * { *; }
-keep @org.koin.core.annotation.Factory class * { *; }
# Koin @Single/@Factory keeps are provided app-wide by config/proguard/shared-rules.pro
# (applied to the app that consumes this module). Re-add them here only if feature:car is
# ever published/consumed standalone.

View File

@@ -30,6 +30,7 @@ mokkery = "3.4.1"
junit5 = "6.1.0"
junit-platform = "6.1.0" # aligned with junit5 — JUnit Platform uses 1.x scheme
kotest = "6.2.1"
konsist = "0.17.3"
testRetry = "1.6.5"
turbine = "1.2.1"
@@ -238,6 +239,7 @@ junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher
kotest-assertions = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" }
kotest-property = { module = "io.kotest:kotest-property", version.ref = "kotest" }
kotest-runner-junit6 = { module = "io.kotest:kotest-runner-junit6", version.ref = "kotest" }
konsist = { module = "com.lemonappdev:konsist", version.ref = "konsist" }
robolectric = { module = "org.robolectric:robolectric", version = "4.16.1" }
turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" }

View File

@@ -106,6 +106,7 @@ include(
":core:datastore",
":core:di",
":core:domain",
":core:konsist",
":core:model",
":core:navigation",
":core:network",