Core concepts
AI Checks
5 min read
Every source you upload runs through six AI-powered checks before you trigger a build. The checks read your source code with an LLM and surface findings in the dashboard: errors, warnings, and info-level suggestions.
Advisory only — checks never block your build
AI checks are advisory. No check status blocks the Build button. A check can mark your source as passed, warned, or failed — in every case, you can still ship. The dashboard makes the verdict visible (red banner on failed, amber on warned) and shows a confirmation prompt when you click Build with outstanding errors or warnings, but the final decision is always yours.
We chose this default because LLM-driven checks have a non-trivial false-positive rate. Hard-blocking shipping on every flagged finding would mean you'd hit "fix the AI" friction more often than "the AI caught a real bug" — and the cost of either calls is much lower than the cost of a missed release. Treat the verdict like a thoughtful but inexperienced reviewer: read what it found, accept what's real, ignore what's noise, and ship.
The six checks
Each check runs as its own LLM call with a focused rubric. They share input (your source bundle, capped at 200 KB) but compute independently — a failure in one does not affect the others. The model used depends on the check: the heavier reasoning checks use Claude Sonnet, the lighter pattern-matching checks use Claude Haiku.
Security scan
Looks for hard-coded server-side secrets in client-bundled code (Stripe sk_live_*, Supabase service_role keys, AWS credentials), injection vulnerabilities (template-string SQL, command injection sinks), authentication bypasses (trusting client-supplied identity), and hard-coded user credentials. Public configuration that's meant to be in the client (Supabase anon key, Firebase web config, publishable Stripe keys, anything prefixed NEXT_PUBLIC_ / VITE_ / EXPO_PUBLIC_) is explicitly excluded from the false-positive set.
Compatibility scan
Detects your app type first (mobile native / web / Electron) and then audits whether the code can actually run on the declared minimum platform versions. Catches things like minSdkVersion 19 with code that calls API 26+ symbols, deprecated APIs removed in newer OS versions, React Native major-version mismatches between packages, and Node engines constraints that contradict the actual imports.
Permission audit
Cross-references declared permissions against actual usage. On mobile: dangerous permissions declared but never used, missing runtime permission flows on Android 6+, background-location with no foreground service. On web: Notification.requestPermission outside a user gesture, overly broad PWA manifest scope. On Electron: nodeIntegration: true in the renderer.
Performance check
Catches definite memory leaks (event listeners not cleaned up, subscriptions never unsubscribed), synchronous blocking operations on the main/request thread (fs.readFileSync in a render path), and bundle-weight footguns (top-level import _ from "lodash" instead of lodash/get). Anti-patterns that degrade UX but don't leak — inline JSX handlers, ScrollView rendering long lists — surface as warnings rather than errors.
Asset validation
Verifies that referenced assets actually exist in the bundle, all required icon sizes and density variants are present, splash screens are declared, and fonts referenced from code are registered in the app config. Catches the "I shipped without the launcher icon" footgun.
Accessibility review
Checks for missing accessibility labels on interactive elements (icon-only buttons, Pressable / TouchableOpacity with no accessibilityLabel), form inputs without associated <label> / aria-label / aria-labelledby, touch targets below the iOS 44×44pt / Android 48×48dp minimum, and state conveyed only by color. Placeholder text does not count as a label.
Severity meanings
| Severity | What it means | Example |
|---|---|---|
| error | The check believes this is a real, blocking-quality issue. Surfaces in the red banner. | A hard-coded sk_live_* Stripe key in a client-bundled module. |
| warning | A real concern you should address before shipping, but not unconditionally severe. Surfaces in the amber banner. | Plain-HTTP login endpoint, console.log of a JWT, missing density variants. |
| info | Best-practice suggestion. Surfaces in the panel but no banner. | An unused asset bloating the bundle, naming suggestions. |
Severity is set by the LLM per finding. The source-level status (passed / warned / failed) aggregates: any error finding → failed; any warning (and no error) → warned; only info (or no findings) → passed.
Running checks
Checks run when you click Run AI Check on the release page after uploading a source — they never start on their own. All six checks fan out together and typically complete in 2–3 minutes.
You can rerun a single check (useful after fixing one issue type) or rerun all checks at once via the Rerun All button on the AI Check panel. Each rerun creates a fresh Check row and re-enqueues the LLM call — older runs stay visible in the per-check history.
Skipping the AI checks entirely
If you don't need the AI verdict for a particular build — say you're iterating quickly and the previous run already covered what changed — you can click Skip & Start Build while checks are still in not_started state. The release will proceed immediately and the checks won't run at all for that release.
What the AI cannot see
The bundle that reaches the LLM is capped at 200 KB and prioritizes manifests + source files relevant to the specific check. The checks don't see binary file sizes, so a 5 MB PNG won't be flagged by asset_validation as oversized. They also can't run your build or your tests — purely static analysis on the source text.
If you want stricter guarantees about what ships, run your own tests in CI before uploading to Lunadeck. The AI is a sanity check on top of that, not a replacement for it.