From 5b2f992d3f4a1db4b17c2dbcbc8f6ad7860518c4 Mon Sep 17 00:00:00 2001 From: Fares Nabil Date: Mon, 22 Jun 2026 03:14:32 +0300 Subject: [PATCH] Add luciq plugin to marketplace catalog Luciq mobile observability plugin, vendored as a local source under external_plugins/luciq. Validated: validate-catalog.py OK; generate-plugin-index.py --check OK. --- .grok-plugin/marketplace.json | 12 + .grok-plugin/plugin-index.json | 38 ++ .../luciq/.claude-plugin/plugin.json | 25 + external_plugins/luciq/.mcp.json | 8 + external_plugins/luciq/assets/logo.svg | 17 + .../luciq/commands/luciq-debug.md | 5 + .../luciq/commands/luciq-verify.md | 5 + .../luciq/skills/luciq-debug/SKILL.md | 171 +++++++ .../luciq/skills/luciq-migrate/SKILL.md | 121 +++++ .../luciq/skills/luciq-setup/SKILL.md | 351 ++++++++++++++ .../luciq/skills/luciq-verify/README.md | 405 ++++++++++++++++ .../luciq/skills/luciq-verify/SKILL.md | 417 +++++++++++++++++ .../luciq-verify/references/check-catalog.md | 220 +++++++++ .../references/extractors-android.md | 206 +++++++++ .../references/extractors-flutter.md | 119 +++++ .../luciq-verify/references/extractors-ios.md | 191 ++++++++ .../luciq-verify/references/extractors-rn.md | 127 +++++ .../references/harness-contract.md | 311 +++++++++++++ .../references/payload-schemas.md | 433 ++++++++++++++++++ .../references/rule-pack-format.md | 277 +++++++++++ .../references/static-checks-catalog.md | 165 +++++++ 21 files changed, 3624 insertions(+) create mode 100644 external_plugins/luciq/.claude-plugin/plugin.json create mode 100644 external_plugins/luciq/.mcp.json create mode 100644 external_plugins/luciq/assets/logo.svg create mode 100644 external_plugins/luciq/commands/luciq-debug.md create mode 100644 external_plugins/luciq/commands/luciq-verify.md create mode 100644 external_plugins/luciq/skills/luciq-debug/SKILL.md create mode 100644 external_plugins/luciq/skills/luciq-migrate/SKILL.md create mode 100644 external_plugins/luciq/skills/luciq-setup/SKILL.md create mode 100644 external_plugins/luciq/skills/luciq-verify/README.md create mode 100644 external_plugins/luciq/skills/luciq-verify/SKILL.md create mode 100644 external_plugins/luciq/skills/luciq-verify/references/check-catalog.md create mode 100644 external_plugins/luciq/skills/luciq-verify/references/extractors-android.md create mode 100644 external_plugins/luciq/skills/luciq-verify/references/extractors-flutter.md create mode 100644 external_plugins/luciq/skills/luciq-verify/references/extractors-ios.md create mode 100644 external_plugins/luciq/skills/luciq-verify/references/extractors-rn.md create mode 100644 external_plugins/luciq/skills/luciq-verify/references/harness-contract.md create mode 100644 external_plugins/luciq/skills/luciq-verify/references/payload-schemas.md create mode 100644 external_plugins/luciq/skills/luciq-verify/references/rule-pack-format.md create mode 100644 external_plugins/luciq/skills/luciq-verify/references/static-checks-catalog.md diff --git a/.grok-plugin/marketplace.json b/.grok-plugin/marketplace.json index 55518e0..b1f119b 100644 --- a/.grok-plugin/marketplace.json +++ b/.grok-plugin/marketplace.json @@ -105,6 +105,18 @@ "homepage": "https://github.com/firecrawl/firecrawl-grok-plugin", "keywords": ["firecrawl", "fire crawl"], "domains": ["firecrawl.dev", "docs.firecrawl.dev"] + }, + { + "name": "luciq", + "description": "Official Luciq plugin (MCP server + skills) for mobile observability. Investigate production crashes, hangs, and bug reports, trace root causes, integrate and migrate the Luciq SDK, and verify SDK upgrades across iOS, Android, Flutter, React Native, and KMP, directly from your coding agent.", + "category": "monitoring", + "source": { + "type": "local", + "path": "./external_plugins/luciq" + }, + "homepage": "https://luciq.ai", + "keywords": ["luciq", "instabug", "luciq sdk", "luciq mcp"], + "domains": ["luciq.ai", "api.luciq.ai", "dashboard.luciq.ai", "docs.luciq.ai"] } ] } diff --git a/.grok-plugin/plugin-index.json b/.grok-plugin/plugin-index.json index aa0d5d7..106468f 100644 --- a/.grok-plugin/plugin-index.json +++ b/.grok-plugin/plugin-index.json @@ -168,6 +168,44 @@ ] } }, + "luciq": { + "components": { + "commands": [ + { + "name": "luciq-debug", + "description": "Investigate a Luciq production signal end to end (crash, hang, bug report, performance regression, App Store rating dro…" + }, + { + "name": "luciq-verify", + "description": "Verify a Luciq SDK upgrade end to end before shipping (invokes the luciq-verify skill)." + } + ], + "mcpServers": [ + { + "name": "luciq", + "description": "http" + } + ], + "skills": [ + { + "name": "luciq-debug", + "description": "Use when the user wants to investigate a Luciq production signal end to end, propose a code fix, or answer \"why is this…" + }, + { + "name": "luciq-migrate", + "description": "Use when the user asks to migrate a mobile codebase from the legacy Instabug SDK to Luciq, upgrade between Luciq SDK ve…" + }, + { + "name": "luciq-setup", + "description": "Use when the user asks to add, install, set up, integrate, or initialize the Luciq mobile observability SDK in an iOS,…" + }, + { + "name": "luciq-verify", + "description": "Verify a Luciq SDK upgrade end to end before shipping. Confirms the customer's custom integration (URL redirection, mas…" + } + ] + } + }, "mongodb": { "sha": "9ea7387c7a1638604542c6efd52e5efc6a7fc393", "components": { diff --git a/external_plugins/luciq/.claude-plugin/plugin.json b/external_plugins/luciq/.claude-plugin/plugin.json new file mode 100644 index 0000000..f28066b --- /dev/null +++ b/external_plugins/luciq/.claude-plugin/plugin.json @@ -0,0 +1,25 @@ +{ + "name": "luciq", + "description": "Luciq mobile observability SDK skills. Investigate bugs, generate RCAs, and integrate the Luciq SDK into iOS, Android, Flutter, React Native, and KMP projects.", + "version": "1.0.0", + "author": { + "name": "Luciq", + "url": "https://luciq.ai" + }, + "homepage": "https://docs.luciq.ai/product-guides-and-integrations/product-guides/ai-features/agent-skills/", + "repository": "https://github.com/luciqai/agent-skills", + "license": "Apache-2.0", + "logo": "assets/logo.svg", + "keywords": [ + "luciq", + "mobile-observability", + "crash-reporting", + "bug-reporting", + "ios", + "android", + "flutter", + "react-native", + "kmp", + "mcp" + ] +} diff --git a/external_plugins/luciq/.mcp.json b/external_plugins/luciq/.mcp.json new file mode 100644 index 0000000..d37b6cf --- /dev/null +++ b/external_plugins/luciq/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "luciq": { + "type": "http", + "url": "https://api.luciq.ai/api/mcp" + } + } +} diff --git a/external_plugins/luciq/assets/logo.svg b/external_plugins/luciq/assets/logo.svg new file mode 100644 index 0000000..0a5cac7 --- /dev/null +++ b/external_plugins/luciq/assets/logo.svg @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/external_plugins/luciq/commands/luciq-debug.md b/external_plugins/luciq/commands/luciq-debug.md new file mode 100644 index 0000000..93cf3d3 --- /dev/null +++ b/external_plugins/luciq/commands/luciq-debug.md @@ -0,0 +1,5 @@ +--- +description: Investigate a Luciq production signal end to end (crash, hang, bug report, performance regression, App Store rating drop) and propose a code fix (invokes the luciq-debug skill). +--- + +Invoke the `luciq-debug` skill via the Skill tool. Pass any arguments the user provided after the command as the skill's `args`. diff --git a/external_plugins/luciq/commands/luciq-verify.md b/external_plugins/luciq/commands/luciq-verify.md new file mode 100644 index 0000000..0336dc0 --- /dev/null +++ b/external_plugins/luciq/commands/luciq-verify.md @@ -0,0 +1,5 @@ +--- +description: Verify a Luciq SDK upgrade end to end before shipping (invokes the luciq-verify skill). +--- + +Invoke the `luciq-verify` skill via the Skill tool. Pass any arguments the user provided after the command as the skill's `args`. diff --git a/external_plugins/luciq/skills/luciq-debug/SKILL.md b/external_plugins/luciq/skills/luciq-debug/SKILL.md new file mode 100644 index 0000000..239db48 --- /dev/null +++ b/external_plugins/luciq/skills/luciq-debug/SKILL.md @@ -0,0 +1,171 @@ +--- +name: luciq-debug +description: Use when the user wants to investigate a Luciq production signal end to end, propose a code fix, or answer "why is this happening". Triggers include pasting a crash ID, fingerprint, or stack trace; mentioning a Luciq bug number, hang, or ANR; asking "what broke since version X"; flagging a rating drop or review spike; or asking why a session crashed, hung, or terminated. Pulls evidence via the Luciq MCP server, maps it to local source, forms an evidence-cited hypothesis. +--- + +# Luciq Production Debugging + +Investigate a Luciq production signal end to end. Default to evidence-based reasoning. Cite the MCP tool result that supports each claim. If a query returns nothing, surface that fact instead of filling in plausible-looking guesses. + +## When NOT to use this skill + +- First-time SDK install or wiring `Luciq.start(...)`, use `luciq-setup`. +- Renaming Instabug symbols to Luciq, or upgrading between Luciq SDK versions, use `luciq-migrate`. +- General mobile debugging where Luciq is not the data source. This skill is grounded in what the Luciq MCP exposes; without that, do not pretend to use it. + +If the user's request fits any of the above, STOP and route them to the right skill rather than running this one. + +## Prerequisites + +The Luciq MCP server must be configured and authenticated. If MCP tools are not available, STOP and direct the user to https://docs.luciq.ai/product-guides-and-integrations/product-guides/ai-features/luciq-mcp-server/setup-by-ide for setup, or run `luciq-setup` to wire it. + +The MCP exposes (verbatim names): + +| Tool | Purpose | +| --- | --- | +| `list_applications` | List apps and their tokens for the authenticated user. | +| `list_crashes` | List crash groups with filters (version, OS, date range). | +| `crash_details` | Full details for a crash group: top frames, occurrence sample, distributions. | +| `crash_patterns` | Distribution by `pattern_key` (e.g. `oses`, `app_versions`, `devices`). | +| `list_occurrences_tokens` | Occurrence ULIDs for a crash group, paginated. | +| `get_occurrence_details` | Per-occurrence detail: session profiler, logs URLs, device state. | +| `list_app_hangs` | Hang and ANR groups. iOS surface as `FATAL_UI_HANG`, Android as `ANDROID_FATAL_HANG`. | +| `list_bugs` | User-reported bugs. | +| `bug_details` | Full bug detail including compressed log archive URLs. | +| `list_reviews` | App Store / Play Store reviews filtered by `rating` and `app_version`. | + +YOU MUST cite which of these produced any piece of evidence in your hypothesis. Do not invent capabilities the MCP does not expose. See "Out of scope" below for what the MCP deliberately does not return. + +## Workflow + +Run the following loop. Every step is gated on evidence. + +### Step 1. Identify the entry point + +Determine the kind of signal being debugged. If the user has not specified, ask. Do not pick at random. + +| Entry point | Required input | First MCP tool to call | +| --- | --- | --- | +| Crash group | Crash number, fingerprint, or pasted stack trace | `crash_details` (or `list_crashes` to find it first) | +| Specific occurrence of a crash | Crash number plus ULID | `get_occurrence_details` | +| App hang or ANR | Hang number, or "recent UI hangs" | `list_app_hangs` | +| User-reported bug | Bug number | `bug_details` | +| Regression between versions | Two version numbers | `list_crashes` filtered by version, then `crash_patterns` with `pattern_key: app_versions` | +| Review or rating signal | Date range and version | `list_reviews` filtered by `rating` and `app_version` | + +### Step 2. Pull MCP context + +Sequence the available Luciq MCP tools deliberately for the entry point: + +- Crashes: `list_crashes`, `crash_details`, `crash_patterns`, then `list_occurrences_tokens` and `get_occurrence_details` for one or more sessions. +- Hangs: `list_app_hangs` filtered to the recent window. iOS hangs surface as `FATAL_UI_HANG`, Android as `ANDROID_FATAL_HANG`. +- Bug reports: `list_bugs` then `bug_details`. The response includes URLs to compressed logs (network, console, session profiler) when available. +- Regressions: filter `list_crashes` by the two versions, diff the result, then call `crash_patterns` with `pattern_key: app_versions` for the highest-impact new groups. +- Review signals: `list_reviews` filtered to low ratings, then correlate with crash and hang activity in the same window. + +### Step 3. Symbolicate if obfuscated + +If the top frame is a hex address, an obfuscated symbol, or ``, the build is missing its symbol artifact (dSYM for iOS, R8/ProGuard mapping for Android, split-debug-info for Flutter, source map for React Native). STOP and direct the user to upload symbols before continuing. Do not reason over hex addresses. + +### Step 4. Map the top frame to local source + +For the symbolicated top frame: + +- `Grep` the symbol (class plus method) across the project. +- `Read` the matched file with a small window around the offending line (10 lines above and below). +- For multi-platform projects (KMP, RN, Flutter), prefer the platform-specific source set first (`iosMain/`, `androidMain/`). + +If the symbol does not exist locally, the project is at a different commit than the build the crash came from. Surface that fact rather than guessing at a fix. + +### Step 5. Form a hypothesis + +Use this structure exactly. Cite each piece of evidence to the MCP tool that produced it. + +``` +HYPOTHESIS: +CONFIDENCE: + +EVIDENCE: +- Top frame: : - [from: crash_details] +- Distribution: [from: crash_patterns] +- Repro context: [from: get_occurrence_details] +- Correlated signal: [from: list_reviews] + +ROOT CAUSE: +``` + +Confidence is honest. Three corroborating MCP sources is high. Reasoning from the top frame alone is low. + +### Step 6. Propose a fix + +Show a diff. Explain how the fix addresses the root cause. Flag any side effects. Optionally write a failing test that reproduces the issue before applying. Do not apply the diff without user confirmation. + +## Pattern library + +Carry these patterns. Reach for them when the corresponding signature appears in the MCP data. + +### Swift Concurrency (iOS) + +When the top frame involves `async`, `await`, an actor, or a `Sendable` violation: + +- Check whether the crash is `Swift runtime: Fatal error: ...` rather than a typical exception. That is a concurrency-safety check firing. +- Confirm OS distribution via `crash_patterns` with `pattern_key: oses`. Swift 6 strict-concurrency checks behave differently across iOS versions. +- Look at the session profiler from `get_occurrence_details` for hop-to-`@MainActor` patterns near the crash time. +- Do not recommend slapping `@MainActor` on a class to silence the error. Treat that as a smell, not a fix. + +### Android ANRs (`ANDROID_FATAL_HANG`) + +When `list_app_hangs` returns an Android hang: + +- The `crash_cause` field tells you where the main thread was blocked, but not always what blocked it. Pull a few `get_occurrence_details` to see recent main-thread activity and pending I/O. +- Check `pattern_key: app_versions` to see whether the ANR is a regression or a long-tail issue. +- Common offenders: synchronous network calls on the main thread, large `SharedPreferences.commit()` writes, blocking `Lock` acquisitions, work scheduled on the wrong dispatcher. + +### iOS UI hangs (`FATAL_UI_HANG`) + +- The hang `exception` summary indicates duration class. +- Pull the occurrence to confirm what the user was doing. The `current_view` and `app_status` (foreground / background) fields disambiguate. +- Common offenders: synchronous Core Data on `NSManagedObjectContext.viewContext`, file I/O on the main queue, expensive layout work in `viewDidLayoutSubviews`. + +### Out-of-memory crashes + +- OOMs surface as terminations. Check `crash_type` and the exception name. +- Pull the occurrence's `state.memory` and `state.storage` fields from `get_occurrence_details` for resource state at termination. +- Look at `pattern_key: devices`. OOMs concentrate on lower-RAM devices and surface a device-tier story the agent should call out. + +### Network failure correlated crashes + +- For crashes with a stack frame in networking code, pull the occurrence's logs URL from `get_occurrence_details` (compressed log archive). +- Cross-reference with bug reports in the same window via `bug_details`. The `state.logs.network_log` URL often shows the failed request that preceded the crash. +- Do not assume timeout vs DNS failure vs server error without log evidence. The categories matter for the fix. + +## Out of scope + +The skill is grounded in what the Luciq MCP exposes today. It deliberately does not: + +- Compute crash-free session rate or any aggregate metric the MCP does not return. +- Reason about App Store rating drops as a primary investigation entry point. `list_reviews` is correlation, not causation. +- Propose APM regression analysis from MCP data. The MCP does not expose APM span aggregates yet. + +When new MCP tools land (release comparison, APM aggregates, session replay), this skill grows with them. Until then, if the user asks for one of those, say so plainly. + +## Style + +- Do not fabricate stack traces, line numbers, or counts. +- Do not propose a fix without naming the root cause. +- Do not apply edits without showing a diff and getting confirmation. +- If MCP returns nothing for a query, surface that. Do not fill in plausible-looking data. + +## Red Flags - STOP and surface to the user + +If you catch yourself thinking any of these, you are about to ship a fabricated investigation. STOP, surface to the user, do not proceed: + +- "MCP returned nothing, but the user clearly wants an answer, so I'll reason from the symbol name." That is a guess, not a hypothesis. Surface the empty result. +- "The top frame is a hex address but I can probably figure it out from context." Do not. Stop and ask the user to upload symbols. +- "The local symbol doesn't exist but the file looks similar enough." It isn't. The repo is at a different commit; surface that. +- "I'll quote a crash-free session rate from memory." The MCP does not expose that metric. Saying you computed it from MCP data is a fabrication. +- "Confidence is high because the top frame matches my prior." One source is not three. Lower confidence to low or medium. +- "I'll apply the fix without a diff because it's obviously right." Show the diff. Get confirmation. Always. +- "The hypothesis cites the symbol but not which MCP tool produced it." Add the citation, or weaken the hypothesis. + +The pattern: every shortcut here trades "sounds confident" for "actually true." The skill's job is to be true. diff --git a/external_plugins/luciq/skills/luciq-migrate/SKILL.md b/external_plugins/luciq/skills/luciq-migrate/SKILL.md new file mode 100644 index 0000000..633cd2a --- /dev/null +++ b/external_plugins/luciq/skills/luciq-migrate/SKILL.md @@ -0,0 +1,121 @@ +--- +name: luciq-migrate +description: Use when the user asks to migrate a mobile codebase from the legacy Instabug SDK to Luciq, upgrade between Luciq SDK versions, or replace deprecated Luciq APIs. Triggers include phrases like "migrate from Instabug to Luciq", "move us off Instabug", "upgrade Luciq SDK to vX", or "replace deprecated Luciq APIs". Covers iOS, Android, Flutter, React Native, KMP. First-time SDK installs go to luciq-setup. +--- + +# Luciq SDK Migration + +Apply code transforms to migrate or upgrade the Luciq SDK. Drive the workflow off the canonical Migration Hub, not memorized rename tables. Bulk transforms without preview corrupt repos. YOU MUST show three sample diffs before bulk-applying. + +## When NOT to use this skill + +- First-time integration of Luciq into a project that has never used Luciq or Instabug, use `luciq-setup`. +- Investigating a crash, hang, or production signal, use `luciq-debug`. + +If the user's request fits any of the above, STOP and route them to the right skill rather than running this one. + +## Canonical source of truth + +YOU MUST fetch the current rename and deprecation tables from the live Migration Hub before applying any transform. Do not hardcode them in this skill. They go stale every release. + +| Concern | Source | +| --- | --- | +| Instabug-to-Luciq renames, vN-to-vN+1 deprecations, v1-to-v2 API changes | https://docs.luciq.ai/getting-started/luciq-migration-hub | + +## Workflow + +### 1. Refuse to start on a dirty working tree + +Migrations modify source in place. Source must be committed. If `git status` shows uncommitted changes, STOP and ask the user to commit, stash, or explicitly override. No exceptions for "small changes". + +### 2. Detect platform and current SDK + version + +Apply the rules below. First match wins. + +| Platform | Source of truth | What to look for | +| --- | --- | --- | +| iOS | `Podfile.lock` | `Instabug` or `Luciq` pod | +| Android | `app/build.gradle*` plus `gradle.lockfile` | `com.instabug.*` or `ai.luciq.library.*` | +| Flutter | `pubspec.lock` | `instabug_flutter` or `luciq_flutter` | +| React Native | `package-lock.json` or `yarn.lock` | the relevant Instabug or Luciq package | +| KMP | both Android and iOS sources | as above for each side | + +Report: SDK name, current version, count of call sites. The call-site count comes from `Grep` of the old symbol root (for example, `Instabug` for iOS, `com.instabug` for Android). + +### 3. Pick the transform set + +| Intent | Transform set | +| --- | --- | +| Instabug to Luciq | Rename `Instabug*` symbols, imports, packages, dependency entries. | +| vN to vN+1 | Apply known deprecations between those versions. | +| v1 to v2 | v1 to v2 API surface. Fetch the canonical mapping from the Migration Hub. | + +Always look up the current rename and deprecation tables from the Migration Hub above. Do not invent renames. + +### 4. Show three sample diffs before bulk-applying + +This is a hard gate. Do not skip it. + +1. Use `Grep` to find the first three call sites of the old symbol. +2. Generate the diff for each call site. +3. Show all three to the user. +4. Wait for explicit sign-off. + +If the three samples reveal an ambiguity (for example, a renamed method has different parameters in different call sites), STOP, surface the ambiguity, and ask. Do not bulk-apply across an ambiguity. + +### 5. Apply in waves on confirmation + +Apply transforms in this order so the project remains parseable after each wave: + +1. Dependency manifest: `Podfile`, `build.gradle`, `pubspec.yaml`, `package.json`. +2. Imports: every file referencing the old symbol. +3. Type names: class refs, method calls. +4. Project metadata: group names, build phases. + +After each wave, sanity check by opening one sample file and confirming the transform applied cleanly. + +### 6. Run the build to verify + +| Platform | Command | +| --- | --- | +| iOS | `pod install && xcodebuild -workspace .xcworkspace -scheme build` | +| Android | `./gradlew :app:assembleDebug` | +| Flutter | `flutter pub get && flutter analyze && flutter build apk --debug` | +| React Native | `npm install && npx react-native run-android` (or `run-ios`) | +| KMP | both Android and iOS builds | + +Derive `` and `` for iOS as in `luciq-setup`: `xcodebuild -list` to enumerate, ask the user if multiple options exist. + +STOP and surface errors. NEVER claim "done" if the build is broken. + +### 7. Print the manual-review checklist + +For any APIs whose semantics changed beyond a rename (different parameters, callback shapes, default behavior), source the list from the Migration Hub and emit: + +``` +MANUAL REVIEW REQUIRED: +- [ ] : [:] +``` + +These are not auto-applied. The user owns the semantic decision. + +## Style + +- ALWAYS show three sample diffs before bulk-apply. +- ALWAYS verify against the Migration Hub before applying a rename. +- Do not claim "done" if the build is broken. +- Always flag ambiguous renames for manual review. + +## Red Flags - STOP and surface to the user + +If you catch yourself thinking any of these, you are about to corrupt the repo. STOP, surface to the user, do not proceed: + +- "I skipped the three-diff sample because the rename is obvious." It isn't. One ambiguity buried in 200 call sites is a multi-hour cleanup. Show the samples. +- "The working tree was dirty but I figured the changes were unrelated." Migrations interleave with uncommitted work and become impossible to roll back. Refuse and ask. +- "I hardcoded the rename mapping from this file because it looked right." This file is illustrative. The Migration Hub is the source of truth. +- "The build had errors but the rename succeeded, so it's mostly done." It isn't done. Surface the errors verbatim. +- "An ambiguous rename came up but I picked the more common variant." Ambiguity is a stop condition, not a tiebreaker. +- "I bulk-applied across waves without a sanity-check read." Each wave can break parsing for the next. Sanity-check. +- "The manual-review list is long, so I trimmed the low-priority items." The user owns that decision, not the agent. Print the full list. + +The pattern: every shortcut here trades "looks done" for "actually correct." The skill's job is to be correct. diff --git a/external_plugins/luciq/skills/luciq-setup/SKILL.md b/external_plugins/luciq/skills/luciq-setup/SKILL.md new file mode 100644 index 0000000..474abe1 --- /dev/null +++ b/external_plugins/luciq/skills/luciq-setup/SKILL.md @@ -0,0 +1,351 @@ +--- +name: luciq-setup +description: Use when the user asks to add, install, set up, integrate, or initialize the Luciq mobile observability SDK in an iOS, Android, Flutter, React Native, or Kotlin Multiplatform project. Triggers include phrases like "add Luciq", "install Luciq SDK", "set up Luciq", "initialize Luciq", or pasting an empty mobile project and asking to wire Luciq. First-time integration only — for SDK upgrades or migration from the legacy Instabug SDK use luciq-migrate. +--- + +# Luciq SDK Installation + +End-to-end first-time integration of the Luciq mobile observability SDK in a mobile project. Drive every API decision off the canonical platform integration guides linked below. The SDK evolved through the Instabug-to-Luciq rebrand, so any signature memorized in this skill may be stale; always verify against the live guide before applying edits. + +## When NOT to use this skill + +This skill is for first-time SDK integration. Hand off to a sibling skill for any of the following: + +- Upgrading an already-integrated Luciq SDK between versions, or migrating from the legacy Instabug SDK, use `luciq-migrate`. +- Investigating a crash, hang, regression, user-reported bug, or rating drop, use `luciq-debug`. +- Looking up an API signature without installing anything, navigate the live integration guides directly (URLs in the workflow below). + +If the user's request fits any of the above, STOP and route them to the right skill rather than running this one. + +## Canonical sources of truth + +YOU MUST verify SDK API signatures, package names, and MCP transport URLs against these live guides before applying edits. Hardcoded values in this file are illustrative and may be stale. + +| Concern | Source | +| --- | --- | +| iOS install + init | https://docs.luciq.ai/ios/setup-luciq-for-ios/integrate-luciq-on-ios/luciq-ai-ios-guide | +| Android install + init | https://docs.luciq.ai/android/set-up-luciq-for-android/integrate-luciq-on-android/luciq-ai-android-guide | +| Flutter install + init | https://docs.luciq.ai/flutter/setup-luciq-for-flutter/integrating-luciq | +| React Native install + init | https://docs.luciq.ai/react-native/setup-luciq-for-react-native/integrate-luciq-on-react-native | +| KMP install + init | https://docs.luciq.ai/kmp/setup-luciq-for-kmp/integrating-luciq | +| MCP server config | https://docs.luciq.ai/product-guides-and-integrations/product-guides/ai-features/luciq-mcp-server/setup-by-ide | +| App tokens (when authenticated) | Luciq MCP `list_applications` | + +## Workflow checklist + +Track every step. STOP on any failed step. Do not continue past a broken state. + +``` +Setup Progress: +- [ ] 1. Detect platform +- [ ] 2. Acquire app token +- [ ] 3. Run per-platform recipe (deps + init) +- [ ] 4. Configure invocation +- [ ] 5. Configure auto-masking +- [ ] 6. Wire user identification +- [ ] 7. Bootstrap Luciq MCP server +- [ ] 8. Bootstrap Luciq CLI (optional, for symbol upload) +- [ ] 9. Smoke build +- [ ] 10. Hand off summary +``` + +## 1. Detect platform + +Run a single non-recursive Glob at workspace root: `{pubspec.yaml,package.json,*.xcodeproj,*.xcworkspace,build.gradle,build.gradle.kts,shared/build.gradle.kts}`. + +Apply the rules below in this exact order. First match wins. Cross-platform projects contain native subfolders (`ios/Runner.xcodeproj`, `android/build.gradle`), so root-level markers MUST take priority over those. + +1. Root has `pubspec.yaml` -> Flutter (skip iOS/Android subdirs even if present). +2. Root has `package.json` containing `"react-native"` in `dependencies` -> React Native. +3. Root has `shared/build.gradle.kts` with `kotlin("multiplatform")` -> KMP. +4. Root has `*.xcworkspace` or `*.xcodeproj` (and none of the above) -> iOS. +5. Root has `build.gradle` or `build.gradle.kts` (and none of the above) -> Android. + +If two or more rules match unexpectedly (for example, both `pubspec.yaml` and a top-level `*.xcodeproj` outside `ios/`), STOP and ask the user to disambiguate. Do not guess. + +If no rule matches (empty repo, unusual layout, or a project where the entry point lives in a non-standard subdirectory), STOP and ask the user which platform they're targeting and where the project root lives. Do not assume — silently picking a platform here corrupts every downstream step. + +## 2. Acquire app token + +Resolve the token in this order: + +1. Try the Luciq MCP server: `list_applications` returns tokens for apps the authenticated user can see. This works only if Luciq MCP is already authenticated in the user's agent from a previous `luciq-setup` run on another project — for genuine first-time setups, this call will fail with a tool-not-found error and you should fall through to step 2 below. Do not attempt to bootstrap MCP here; that is step 7. +2. Read from environment (`LUCIQ_APP_TOKEN`). +3. Prompt the user. + +NEVER commit the token inline. Use a build-time injection, an env var, or a gitignored secrets file. Tokens leak via git history, which is irreversible. + +## 3. Per-platform recipe + +YOU MUST verify the exact init signature, package name, and Gradle plugin name for the detected platform against the live integration guide above before applying. APIs evolved through the Instabug-to-Luciq rebrand. The recipes below name the files to edit, not authoritative signatures. + +### iOS + +Verify the recommended install method against the live guide before proceeding — the primary method has changed across SDK versions. + +**Swift Package Manager (recommended for all new and existing projects)** + +First check whether a `Package.swift` exists at the project root. + +*Project has `Package.swift`:* +1. Edit `Package.swift` — add to `dependencies` and to the appropriate target's `dependencies`. Verify the repo URL and version on the live guide: + ```swift + // dependencies array: + .package(url: "", from: ""), + // target dependencies: + .product(name: "", package: "luciq-ios-sdk"), + ``` +2. Run `swift package resolve` to fetch. +3. Check the resolved `Package.swift` in DerivedData checkouts to confirm the product name and module name — **they are different**: the SPM product may be `Luciq` while the Swift import is `import LuciqSDK`. Use the module name (from the `.xcframework` contents) for `import`, not the product name. +4. Edit `AppDelegate.swift` (or `.m`): import the module and call the start API. Verify the exact init signature on the live guide. +5. Edit `Info.plist`: add `NSMicrophoneUsageDescription` and `NSPhotoLibraryUsageDescription`. + +*Project is `.xcodeproj`-only (no `Package.swift`):* +SPM works fine for `.xcodeproj` projects via direct `project.pbxproj` edits. Apply all four changes, then run `xcodebuild -resolvePackageDependencies` immediately (no confirmation needed — it only fetches, it does not build): +1. Add an `XCRemoteSwiftPackageReference` entry with the repo URL and `upToNextMajorVersion` requirement. Verify the repo URL on the live guide. +2. Add an `XCSwiftPackageProductDependency` entry pointing to that reference. **Verify the product name from the package's own `Package.swift` after resolving** — it is not the same as the Swift import name. (Confirmed: SPM product = `Luciq`, Swift import = `import LuciqSDK`.) +3. Add the product dependency UUID to `packageProductDependencies` in `PBXNativeTarget`. +4. Add the package reference UUID to `packageReferences` in `PBXProject`. +5. Run `xcodebuild -resolvePackageDependencies -project .xcodeproj`. If it fails with "no versions match", check the actual release tags on the repo and update `minimumVersion` to match (the SDK may be at a high major version, e.g. `19.x`). +6. Edit `AppDelegate.swift` (or `.m`): import the module and call the start API. Verify the exact init signature on the live guide. +7. Edit `Info.plist`: add `NSMicrophoneUsageDescription` and `NSPhotoLibraryUsageDescription`. + +**Carthage (alternative — only if SPM is blocked by a project-level constraint)** +1. Edit (or create) `Cartfile` — verify the binary spec URL on the live guide: + ``` + binary "" + ``` +2. Run `carthage update --use-xcframeworks` after user confirmation. +3. Embed the built `.xcframework` programmatically using the `xcodeproj` Ruby gem: + ```bash + gem install xcodeproj # skip if already installed + ``` + Then run a Ruby script (adapt `TARGET_NAME` and the `.xcframework` filename to the actual project — check `Carthage/Build/` after step 2): + ```ruby + require 'xcodeproj' + project = Xcodeproj::Project.open(Dir.glob('*.xcodeproj').first) + target = project.targets.find { |t| t.name == 'TARGET_NAME' } + ref = project.new_file('Carthage/Build/LuciqSDK.xcframework') + target.frameworks_build_phase.add_file_reference(ref) + phase = target.new_shell_script_build_phase('Copy Luciq Frameworks') + phase.shell_script = '"$(SRCROOT)/Carthage/Build/carthage" copy-frameworks' + phase.input_paths << '$(SRCROOT)/Carthage/Build/LuciqSDK.xcframework' + project.save + ``` + Show the diff of `project.pbxproj` before saving. +4. Edit `AppDelegate.swift` (or `.m`): import the module and call the start API. Verify the exact init signature on the live guide. +5. Edit `Info.plist`: add `NSMicrophoneUsageDescription` and `NSPhotoLibraryUsageDescription`. + +**CocoaPods (deprecated — avoid for new integrations)** +> ⚠️ The CocoaPods registry becomes read-only on December 2, 2026. Prefer SPM or Carthage. Only use this path if the project already uses CocoaPods and migration is out of scope for this task. +1. Edit `Podfile`: add the Luciq pod to the main target — verify the pod name on the live guide. +2. Run `pod install` and `pod update Luciq` after user confirmation. +3. Follow steps 4–5 above. + +### Android + +Verify exact dependency coordinates, version, and init signature against the live guide before applying — these change across releases. + +1. **Check compile SDK version**: must be ≥ 29. Raise `compileSdkVersion` in `app/build.gradle(.kts)` if needed. +2. **Add the dependency** in `app/build.gradle(.kts)` (verify groupId, artifactId, and latest version on the live guide): + - Gradle: `implementation 'ai.luciq.library:luciq:'` + - Maven projects: use the same groupId/artifactId coordinates from the live guide. +3. **Verify dependency resolution** after user confirmation: `./gradlew :app:dependencies` — this triggers Gradle to fetch the new dependency without needing Android Studio. Fix any resolution errors before continuing. +4. **Initialize in the Application subclass** `onCreate` using the Builder pattern (verify exact API on the live guide): + - Kotlin: `Luciq.Builder(this, "APP_TOKEN").build()` + - Java: `new Luciq.Builder(this, "APP_TOKEN").build();` +5. **Permissions**: the SDK automatically injects `WAKE_LOCK` and `INTERNET` into `AndroidManifest.xml` — no manual edits needed. Optional permissions for image/video attachments and network monitoring are listed in the live guide. +6. **Android 15+ (API 35)**: if `targetSdkVersion` is 35 or higher, the live guide requires Luciq ≥ 13.4.0 for 16 KB page-size support. Verify the minimum compatible version on the live guide and pin accordingly. + +### Flutter + +1. **Add dependency** in `pubspec.yaml` (verify the exact package name and version on the live guide): + ```yaml + dependencies: + luciq_flutter: + ``` +2. **Fetch the package**: `flutter packages get` +3. **Import** in the file where you initialize: `import 'package:luciq_flutter/luciq_flutter.dart';` +4. **Initialize** in `initState()` (verify the exact API signature on the live guide): + ```dart + Luciq.init( + token: 'APP_TOKEN', + invocationEvents: [InvocationEvent.shake], + ); + ``` +5. **iOS permissions** — add to `Info.plist` (required for media attachments): + - `NSMicrophoneUsageDescription` + - `NSPhotoLibraryUsageDescription` +6. **Android permissions**: auto-injected into `AndroidManifest.xml` — no manual edits needed. Exception: if you enable screenshot invocation, the SDK requests storage permission at app launch (it monitors the screenshots directory). + +### React Native + +**Requirement:** React Native ≥ 0.60.x. Verify the minimum version on the live guide before proceeding. + +1. **Install the package** (verify the exact package name on the live guide): + - npm: `npm install @luciq/react-native` + - yarn: `yarn add @luciq/react-native` +2. **iOS native deps**: `cd ios && pod install && cd ..` after user confirmation. +3. **Android**: autolinking handles native wiring automatically — no manual step needed. +4. **Initialize** in `index.js` (verify the exact API signature on the live guide): + ```js + import Luciq, { InvocationEvent } from '@luciq/react-native'; + + Luciq.init({ + token: 'APP_TOKEN', + invocationEvents: [InvocationEvent.shake], + }); + ``` +5. **iOS permissions** — add these keys to `info.plist` (required for media attachments): + - `NSMicrophoneUsageDescription` + - `NSPhotoLibraryUsageDescription` + +### KMP + +Verify dependency coordinates, version, and init signatures against the live guide — these change across releases. + +1. **Add the shared dependency** in `shared/build.gradle.kts` under `commonMain` (get the latest version from Maven Central): + ```kotlin + sourceSets { + commonMain.dependencies { + api("ai.luciq-library:luciq-kmp:") + } + } + ``` + iOS also requires a separate native LuciqKMP dependency — check the live guide for the exact artifact. + +2. **Create a shared config object** in `commonMain` (verify the exact class names and fields on the live guide): + ```kotlin + import ai.luciq.kmp.modules.LuciqKmp + import ai.luciq.kmp.utils.InvocationEvents + + object LuciqDefaults { + const val APP_TOKEN = "YOUR_TOKEN" + val invocationEvents = listOf(InvocationEvents.FloatingButton) + } + + fun initializeLuciq(configuration: LuciqConfiguration) { + LuciqKmp.init(configuration) + } + ``` + +3. **Android entry point** — call as early as possible in the Application class, passing the `Application` instance: + ```kotlin + val configuration = LuciqConfiguration( + androidApplication = application, + token = LuciqDefaults.APP_TOKEN, + invocationEvents = LuciqDefaults.invocationEvents, + ) + initializeLuciq(configuration) + ``` + +4. **iOS entry point** — call as early as possible in the app lifecycle (e.g. `application(_:didFinishLaunchingWithOptions:)`); omit `androidApplication`: + ```swift + let configuration = LuciqConfiguration( + token: LuciqDefaults.shared.APP_TOKEN, + invocationEvents: LuciqDefaults.shared.invocationEvents, + ) + initializeLuciq(configuration: configuration) + ``` + +5. **Permissions**: + - Android: the native SDK declares required permissions automatically; remove any that your app does not need. + - iOS: add `NSMicrophoneUsageDescription` and `NSPhotoLibraryUsageDescription` to `Info.plist`. + +6. **Platform-specific extras** (if applicable): Jetpack Compose apps may need additional native Compose libraries for screen tracking and APM; SwiftUI apps may need native SwiftUI APIs. Check the live guide for current requirements. + +## 4. Configure invocation + +Default to shake gesture plus screenshot. Offer alternatives: floating button, two-finger swipe, or programmatic-only. Apply the user's choice. + +## 5. Configure auto-masking + +Goal: identify likely-sensitive UI views and configure SDK-side masking. A naive substring grep produces false positives (validators, comments, test fixtures), so the search must be narrowly scoped and every match must be user-confirmed. + +1. Grep the platform's UI source files only (`*.swift`, `*.kt`, `*.dart`, `*.tsx`, `*.jsx`) for these identifier-shaped strings: `password`, `email`, `cardNumber`, `ssn`, `cvv`, `pin`, `dob`, `iban`. +2. Filter out matches in `*test*`, `*spec*`, `*mock*`, `*fixture*` paths, validator/regex utilities, and anything under `node_modules`, `Pods/`, or `build/`. +3. Show the filtered match list with `file:line` for each. Get per-match confirmation. Do not apply masking rules in bulk. +4. Verify the masking API signature for the detected platform on the live guide. The masking API has differed across platforms and changed across SDK versions; do not hardcode it. +5. Apply masking config only for confirmed matches. + +Also configure network-log redaction: sensitive headers (Authorization, Cookies) and body fields (password, token). + +## 6. Wire user identification + +If the app has authentication, find login and logout flows. Add `identifyUser(...)` and the corresponding sign-out call so reports tie back to your users. Verify the exact identification API on the live guide. + +If the app is anonymous-first (no login surface — typical for many B2C utilities, content readers, and games with guest play), skip this step entirely. Do not synthesize a fake user identity, do not insert `identifyUser` at app launch with placeholder values, and do not block the workflow waiting for a login flow that doesn't exist. Note the skip in the hand-off summary so the user can wire identification later if they add auth. + +## 7. Bootstrap Luciq MCP server + +YOU MUST verify the MCP server URL and transport type against https://docs.luciq.ai/product-guides-and-integrations/product-guides/ai-features/luciq-mcp-server/setup-by-ide before proceeding. Both have evolved across releases. + +Ask the user once: "global or project-local?" — then run immediately. Default to user-global if they don't express a preference. Use the `claude mcp add` CLI. Do NOT hand-edit `~/.claude.json` directly — the file can be very large and a malformed edit will break all MCP servers: + +```bash +# User-global (survives across projects): +claude mcp add --transport http luciq --scope user + +# Project-local (.mcp.json in repo root): +claude mcp add --transport http luciq +``` + +After running, prompt the user to restart their agent (Claude Code, Cursor, Codex, or other supported client) and complete the OAuth flow. Once authenticated, Luciq MCP tools become available qualified as `luciq:` (for example, `luciq:list_crashes`). + +## 8. Bootstrap the Luciq CLI (optional) + +If the project will upload symbol artifacts (dSYMs, ProGuard or R8 mapping files, source maps, or split-debug-info) to Luciq for symbolication of obfuscated frames, install the Luciq CLI. + +YOU MUST verify the install command, supported platforms, and exact upload subcommand on the live integration guide for the user's platform. The CLI's distribution channel and command surface have changed across releases; do not hardcode an install command here. + +Store credentials via environment variables (`LUCIQ_APP_TOKEN` plus any per-platform secrets the live guide names). NEVER commit credentials inline. + +## 9. Smoke build + +| Platform | Command | +| --- | --- | +| iOS | `xcodebuild -project .xcodeproj -scheme -sdk iphonesimulator -destination "generic/platform=iOS Simulator" build` | +| Android | `./gradlew :app:assembleDebug` | +| Flutter | `flutter build apk --debug` | +| React Native (Android) | `npx react-native run-android` | +| React Native (iOS) | `npx react-native run-ios` | +| KMP | run both Android and iOS builds | + +Deriving `` and `` for iOS and RN-iOS: + +- ``: the `.xcodeproj` filename (without extension) at the project root. If a `.xcworkspace` exists instead (e.g. after CocoaPods install), use `-workspace Foo.xcworkspace` instead of `-project`. +- ``: derive by running `xcodebuild -list -project .xcodeproj` (or `-workspace` if applicable) and picking the app scheme. Usually matches the project name. For RN, the scheme typically matches the app's display name in `app.json`. +- If multiple workspaces or schemes exist, STOP and ask the user which to build. Do not guess. + +STOP on build failure. NEVER claim success on a broken build. + +## 10. Hand off + +Print: +- File where init was added. +- Invocation event configured. +- Masking rules applied (with file:line for each). +- User identification call sites. +- MCP / CLI wired status. +- A test command (for example, "shake the device or simulator to invoke Luciq"). +- Pointers: `luciq-debug` for crash investigation, `luciq-migrate` for moving off the legacy Instabug SDK or upgrading between Luciq versions. + +## Style + +- ALWAYS show diffs before applying code edits. +- ALWAYS confirm before running `pod install`, gradle syncs, or build commands. +- Verify SDK API signatures from the live integration guide. Do not hardcode them in this skill. + +## Red Flags - STOP and surface to the user + +If you catch yourself thinking any of these, you are about to ship a broken integration. STOP, surface to the user, do not proceed: + +- "The build failed but the SDK is installed, so it's probably fine." It isn't. A failing build means a broken integration. Report the failure verbatim. +- "I skipped checking the live guide because the docs probably haven't changed." That's how you ship a stale signature. Always verify. +- "I hardcoded the init signature from this file, it looked right." This file is illustrative, not authoritative. The live guide is the source of truth. +- "I committed the app token inline because it's just for local testing." Tokens leak via git history. Use env injection or a gitignored secrets file. +- "I auto-applied the masking rules without showing the user the matches." False positives are likely. Per-match confirmation is mandatory. +- "`pod install` or `gradle sync` had warnings but the build went green." Warnings about Luciq specifically are not cosmetic. Read them, surface them. +- "Two platform markers matched but I picked the obvious one." If the workspace is ambiguous, ask. Cross-platform projects break this assumption routinely. + +The pattern: every shortcut here trades "looks done" for "actually works." The skill's job is to actually work. diff --git a/external_plugins/luciq/skills/luciq-verify/README.md b/external_plugins/luciq/skills/luciq-verify/README.md new file mode 100644 index 0000000..6571967 --- /dev/null +++ b/external_plugins/luciq/skills/luciq-verify/README.md @@ -0,0 +1,405 @@ +# luciq-verify + +A Claude Code / Cursor skill that verifies a Luciq mobile SDK upgrade end-to-end **before** you ship — by smoking your debug build, pulling the resulting telemetry, and auditing it against your custom integration's contract. + +If you've ever upgraded an SDK and asked "did our redaction callbacks still fire? are our custom headers preserved? did the new SDK version sneak any PII into user steps?" — that's this skill. + +--- + +## What it does + +When you bump the Luciq SDK version, several things can silently break: + +- A redaction callback's contract changes — your "replace body with ``" hook compiles but no longer fires. +- A new HTTP client (Ktor / OkHttp release) isn't intercepted — that traffic now ships **unredacted**. +- The SDK adds new auto-instrumentation that captures TextField contents — PII suddenly appears in user steps even though your code didn't change. +- An attribute key gets renamed (`persona` → `user_persona`) — your 5 persona tags stop reporting. +- A feature-flag length cap drops from 70 → 50 — flags get truncated and stop matching the dashboard. + +None of these break a build. None of them fail a unit test. They just silently leak PII or drop signal in production. **This skill catches them.** + +The audit verifies (configurable per customer via `luciq-verify.yaml`): + +- **Network capture**: URL normalization, custom headers preserved, request/response bodies redacted, sensitive headers absent, attachment URL paths masked, task tracer correlation IDs present, no SDK self-traffic. +- **PII**: regex sweep over user steps, attribute values, URL query strings, identity fields. +- **Attributes**: required user attributes (tenant, locale, persona) present; numbered custom-attribute slots populated. +- **Experiments / feature flags**: count above floor, key length within truncation limit. +- **SDK hygiene**: no `warn` or `error` lines from Luciq itself in the app log. +- **Synthetic markers**: the test crash actually landed, breadcrumbs captured at the expected threshold. +- **Environment**: SDK version matches the one under test, debug build pointed at non-prod backend. + +--- + +## How it works + +### Big picture + +```mermaid +sequenceDiagram + autonumber + participant Dev as Developer / CI + participant Skill as luciq-verify skill + participant Device as Device / Simulator + participant App as Debug build (new SDK) + participant Cloud as Luciq cloud + participant MCP as Luciq MCP server + + Dev->>Skill: "verify the upgrade" + Skill->>Device: install build + open deep link + Device->>App: launch harness screen + App->>App: trigger sequence
(persona, network burst,
flags, bug report, crash) + App->>Cloud: ship telemetry + Skill->>MCP: poll 3 channels
(list_crashes, list_bugs, apm_*) + MCP->>Cloud: query + Cloud-->>MCP: occurrence payloads + MCP-->>Skill: structured responses + Skill->>Skill: apply merged rule pack
(base + customer) + Skill->>Dev: HTML + Markdown report +``` + +The skill drives a real SDK in a real app, then audits what actually shipped. No mocks, no test doubles — the dashboard is the oracle. + +### The workflow + +```mermaid +flowchart TD + Start([User invokes skill]) --> P0[0. Detect maturity tier
T0 / T1 / T2 / T3] + P0 --> P1[1. Setup
idempotent] + P1 --> P1a{First run
on this repo?} + P1a -->|Yes| P1b[Scaffold harness into debug variant
Scaffold luciq-verify.yaml
Bootstrap rules from telemetry] + P1a -->|No| P1c[No-op — artifacts exist] + P1b --> P2 + P1c --> P2 + P2[2. Static audit
config + source patterns
skipped if --runtime] + P2 --> P3[3. Pre-flight safety
skipped if --static] + P3 --> P3a{Debug variant?
Non-prod backend?
MCP reachable?
Device available?} + P3a -->|No| Stop([STOP + surface reason]) + P3a -->|Yes| P4[4. Smoke
skipped if --static] + P4 --> P4a[Install build → open deep link
→ run trigger sequence
→ flushNow → forceCrash] + P4a --> P4b[Poll 3 channels in parallel
up to 90s] + P4b --> P5[5. Runtime audit
merged rule pack vs payload
skipped if --static] + P2 --> P6 + P5 --> P6[6. Report + drift detection] + P6 --> Out([HTML + Markdown report
+ proposed rule-pack diff]) +``` + +### Three audit channels, picked by preference + +| Channel | What it gives | When it's used | +| --- | --- | --- | +| **APM** (`apm_*` MCP tools) | Per-request structured network data, with built-in failure-rate split | Primary for redaction / headers (iOS + Android only) | +| **Bug** (`bug_details` MCP tool) | Network log, breadcrumbs, and SDK log as **separate** named archives | Cleanest fallback after APM; available on every platform | +| **Crash** (`get_occurrence_details` MCP tool) | All session data bundled into one archive that needs parsing apart | Always used for the synthetic crash itself, attributes, environment, occurrence identity | + +The skill polls all three channels in parallel during the smoke and prefers APM > Bug > Crash for network audit. If APM isn't GA on your account, the bug channel covers it. If both are unavailable, the crash channel's `compressed_logs` archive is parsed as a fallback. + +```mermaid +flowchart LR + subgraph Smoke["Smoke phase produces"] + Crash[Crash occurrence
+ session payload] + Bug[Bug record
+ split log archives] + APM[APM groups
+ per-request data] + end + + Crash --> Audit{Audit picks
by preference
APM > Bug > Crash} + Bug --> Audit + APM --> Audit + + Audit -->|APM available
iOS / Android| UseAPM["Use APM
structured per-request data
+ failure-rate split"] + Audit -->|APM N/A
e.g. Flutter / RN| UseBug["Use bug network_log
dedicated typed archive"] + Audit -->|Bug also absent| UseCrash["Parse compressed_logs
bundled archive fallback"] + + UseAPM --> Out[C1-C7 evidence] + UseBug --> Out + UseCrash --> Out +``` + +For everything other than network capture — synthetic markers, attributes, occurrence identity, environment — the crash channel is always used because that's where `current_view`, `state.fields.app_version`, `state.fields.sdk_version`, and the synthetic marker live. + +### Dashboard as oracle + +The skill doesn't write unit tests or mock the SDK. It drives your debug build through a deterministic harness, lets the **real SDK in a real app** ship telemetry to the Luciq backend, then pulls that telemetry back via the Luciq MCP server and asserts against it. End-to-end behavioral verification. + +### The phases + +``` +0. Detect maturity tier — T0 / T1 / T2 / T3 +1. Setup — idempotent; scaffolds the harness and rule pack on first run +2. Static audit — agent-native source + config inspection (skipped if --runtime) +3. Pre-flight — safety: SDK version, debug variant, non-prod backend, MCP reachable +4. Smoke — drive the harness, wait for the occurrence to land +5. Runtime audit — apply rules from the merged rule pack against the captured payload +6. Report — render HTML + Markdown report; propose rule-pack drift updates +``` + +First run does heavy setup (harness scaffold, rule-pack bootstrap, environment confirmation). Every subsequent SDK upgrade is a 2-minute "press go." + +### Three invocation flags + +Default invocation runs every phase. Flags trim scope when only part of the audit is needed: + +| Flag | Phases run | When to use | +| --- | --- | --- | +| (none) | 0, 1, 2, 3, 4, 5, 6 | Full audit — static + runtime in one report. Default for SDK upgrades. | +| `--static` | 0, 1, 2, 6 | Static config inspection only. Useful as a pre-upgrade snapshot or anytime the user wants "is my integration wired correctly" without driving the device. | +| `--runtime` | 0, 1, 3, 4, 5, 6 | Skip the static phase; runtime smoke + MCP audit only. Useful when static config has already been validated. | + +### What the static audit covers (Phase 2) + +Agent-native inspection of source files and build config. No external runtimes, no Python, no scanning daemon. Findings cite file path + line range; matched text is never quoted unless it's a known-safe identifier. + +| Family | Examples | +| --- | --- | +| `S-INSTALL-*` | SDK declared in manifest, version pinned, init call site present | +| `S-MODULE-*` | Per-module enable/disable across 14 SDK modules (BR, Crash, APM, SR, NLG, User Steps, ANR, OOM, NDK, Surveys, Replies, Feature Requests, Force Restart, Network Auto-Masking) | +| `S-INVOKE-*` | Invocation events configured, programmatic invocation, no `.none` overrides | +| `S-IDENTITY-*` | User identification + custom data + attribute hooks present | +| `S-FLAG-*` | Feature flag API usage (add / remove / clear / check) | +| `S-LOG-*` | Custom logging + user-event logging in use | +| `S-MASK-*` | Network auto-masking, screen masking, sensitive header config | +| `S-SYMBOL-*` | iOS dSYM upload pipeline; Android mapping upload plugin | +| `S-BUILD-*` | Build system detection (SPM / CocoaPods / Carthage / Gradle / npm / pub) | +| `S-PRIVACY-*` | iOS view-modifier privacy markers (SwiftUI / UIKit) | + +Per-platform extractor recipes live in `references/extractors-{ios,android,flutter,rn}.md`. The full S-* catalog lives in `references/static-checks-catalog.md`. + +### Two harness modes — scaffold or reuse + +The skill supports two ways of getting deterministic triggers into your debug build. Pick one in your `luciq-verify.yaml`: + +**Scaffold (default).** The skill writes a small harness directly into your debug variant — *not* a published package. Per platform: + +- **iOS**: `/DebugOnly/LuciqVerify/LuciqVerifyHarness.swift` with `#if DEBUG` guards +- **Android**: `app/src/debug/java//luciqverify/` (debug sourceSet only) +- **Flutter**: `lib/luciq_verify/` mounted only under `kDebugMode` +- **React Native**: `src/luciq-verify/` gated by `if (__DEV__)` +- **KMP**: `shared/src/debugMain/` plus thin platform shims + +The scaffolded harness exposes a small API (`setTestPersona`, `fireNetworkBurst`, `exerciseFeatureFlags`, `reportBugReport`, `forceCrash`, `forceANR`, `forceUIHang`, `flushNow`) plus a one-button-per-trigger screen reachable via `luciq://luciq-verify-harness` in debug builds only. + +**Reuse.** Already have a debug menu with crash / hang / bug triggers (e.g. a `DevToolsFragment` or a `CrashLab` / `HangTrigger` / `ErrorTrigger` family)? Declare it instead of scaffolding a parallel one. Your rule pack maps the canonical triggers to your existing methods, and declares **how** each trigger should be invoked. + +Four invocation strategies, picked per trigger by `invoke_via`: + +```mermaid +flowchart TD + Trig[Canonical trigger
e.g. forceCrash] --> Choice{invoke_via?} + Choice -->|deep_link_param| DL["URL parameter
yourapp://devtools?trigger=forceCrash

Cleanest. Most modern dev menus."] + Choice -->|intent_extra| IE["Android intent extra
am start ... --es trigger forceCrash

Common in older Android dev menus."] + Choice -->|tap_by_label| TBL["mobile-mcp reads accessibility tree,
finds button by label, taps it

Optional, requires mobile-mcp.
Best for legacy dev menus."] + Choice -->|manual| MAN["Skill prints sequence, user taps

Always available fallback."] +``` + +```yaml +harness: + mode: reuse + reused_surface: + marker_view: "DevToolsFragment" # what current_view is on occurrences from this screen + deep_link: "myapp://devtools" # optional, for hands-free smoke + triggers: + # Shorthand → invoke_via: manual + setTestPersona: "PersonaTrigger.setTestPersona" + + # Object form with explicit strategy + forceCrash: + method: "CrashTrigger.forceUnwrapNil" + invoke_via: "intent_extra" + param_name: "trigger" + param_value: "forceUnwrapNil" + + reportBugReport: + method: "BugTrigger.reportFromDevTools" + invoke_via: "tap_by_label" # requires mobile-mcp (optional) + label: "Report Bug" + + flushNow: "LuciqSDK.flushNow" +``` + +The skill verifies the marker view actually surfaces in past occurrences, checks the surface is debug-gated, then drives your existing methods during the smoke. Unmapped triggers become no-ops and the rules that needed them SKIP — the audit degrades gracefully. + +Either mode produces `luciq-verify.yaml` at the repo root — your rule pack, where customer-specific rules live (redaction tokens, allowed hosts, required headers, attribute schema, PII regex). + +--- + +## How to use it + +### Prerequisites + +The skill has **hard dependencies** (it refuses to run without them) and **optional integrations** (it gains capabilities when they're present but works fine without). + +```mermaid +flowchart LR + subgraph Hard["Hard dependencies — skill refuses to run without"] + L[Luciq MCP server
authenticated] + B[Debug build
with new SDK] + D[Device / simulator / emulator
booted] + end + subgraph Optional["Optional integrations — capabilities unlocked when present"] + M[mobile-mcp server
github.com/mobile-next/mobile-mcp] + end + + L --> Skill[luciq-verify] + B --> Skill + D --> Skill + M -.optional.-> Skill + + M -.->|enables| F1["tap_by_label trigger invocation
(reuse mode, drives existing dev menus
without intent extras / deep-link params)"] + M -.->|enables| F2["diagnostic screenshots
(end-of-smoke + on-timeout)"] +``` + +**Hard dependencies:** + +1. **Luciq MCP server, authenticated.** The entire audit is grounded in what the Luciq MCP exposes — `list_applications`, `list_crashes`, `list_bugs`, `list_occurrences_tokens`, `get_occurrence_details`, `bug_details`, `crash_patterns`, and (when GA) `apm_*`. Without it the skill has no oracle to verify against. Run `luciq-setup` first if you haven't, or follow https://docs.luciq.ai/product-guides-and-integrations/product-guides/ai-features/luciq-mcp-server/setup-by-ide. +2. **A debug-variant build with the new SDK.** The skill bumps no dependencies — it audits whatever's in your lockfile. +3. **A booted device, simulator, or emulator.** The skill drives the build via deep link (or via mobile-mcp when present); nothing for you to tap manually unless reuse mode falls back to manual triggers. + +**Optional integrations:** + +- **[mobile-mcp](https://github.com/mobile-next/mobile-mcp).** Adds two capabilities: + - **`tap_by_label` triggers** in reuse mode. If your existing dev-tools menu doesn't accept intent extras or deep-link params (legacy dev menus often don't), mobile-mcp reads the device's accessibility tree and taps your buttons by label — no app changes required to make reuse mode hands-free. + - **Diagnostic screenshots.** End-of-smoke screenshots (proof your harness was reachable) and timeout screenshots (what was on screen when no occurrence landed). Opt in via `optional_integrations.mobile_mcp.screenshot_on_smoke_end` / `screenshot_on_smoke_timeout`. + + Without mobile-mcp, `tap_by_label` triggers degrade to manual (the skill prints the trigger sequence; you tap) and screenshots are simply not captured. Pre-flight still passes — unless your rule pack sets `optional_integrations.mobile_mcp.enabled: force`. + +### What the skill checks before doing anything + +```mermaid +flowchart TD + Invoke([Skill invoked]) --> Detect[Phase 0:
Detect maturity tier] + Detect --> Q1{Harness scaffolded
in debug variant?} + Q1 -->|No| Q2{Any telemetry from
previous SDK version?} + Q1 -->|Yes| Q3{luciq-verify.yaml
exists?} + Q2 -->|No| T0[Tier T0 — empty
Run setup, then ask user
to produce one occurrence
and re-invoke] + Q2 -->|Yes| T1[Tier T1 — telemetry only
Audit organic occurrence
S* synthetic rules SKIP
C0b recency becomes WARN] + Q3 -->|No| T2[Tier T2 — harness only
Deterministic audit against
base rule pack only
A* customer rules SKIP] + Q3 -->|Yes| T3[Tier T3 — full
Deterministic audit with
customer-specific rule pack
Full end state] + + T0 --> Out + T1 --> Out + T2 --> Out + T3 --> Out[Continue to Phase 1+] +``` + +The skill reports the detected tier explicitly — users often don't know which tier they're in until told. + +### First run + +**The fastest invocation is the slash command** — type `/luciq-verify` and the skill takes over. You can pass arguments after it (e.g. `/luciq-verify 19.6.0`) and they're forwarded as the skill's `args`. + +The skill also auto-activates on natural-language trigger phrases like: + +- "verify the Luciq upgrade" +- "audit Luciq 19.6.0" +- "did the SDK bump break anything" +- "run upgrade verification" +- "is it safe to release this version" +- "smoke the new Luciq SDK" + +What happens on first run: + +1. The skill detects your platform and asks for confirmation before doing anything that writes files. +2. **Harness setup, branching on your rule-pack `harness.mode`:** + - **`scaffold` (default)** — generates `LuciqVerifyHarness.` into your debug variant and shows you the diff. + - **`reuse`** — validates the `reused_surface` declaration in your rule pack: confirms the `marker_view` exists on prior occurrences, checks the surface is debug-gated, surfaces gaps. No source generation. +3. It scaffolds `luciq-verify.yaml` at the repo root if it doesn't exist. If you have ≥ 10 prior occurrences on the previous SDK version, it proposes a rule pack populated from your observed telemetry. You review and accept block-by-block. +4. It runs pre-flight safety checks (debug variant, non-prod backend, MCP reachable, device available). +5. It launches your harness (deep link if available, otherwise activity intent, otherwise prompts you), fires the canonical trigger sequence, and polls the three audit channels in parallel. +6. It audits the payload and renders the report. + +The rule pack lives in your repo and is version-controlled like any other file. Edit it directly when your contract changes. + +### Subsequent runs + +After the first run, invoking the skill again skips setup (harness is already there, rule pack exists) and goes straight to pre-flight → smoke → audit → report. The skill also runs **drift detection** — if it sees new attribute keys, new headers, new SDK API call sites, or rules that haven't fired in N runs, it proposes a unified diff against your rule pack for you to accept or reject. + +### Reading the report + +Two artifacts are produced in your repo root: + +- `luciq-verify-report.html` — colored status pills, expandable evidence rows, network audit table. Open in a browser. +- `luciq-verify-report.md` — same content as Markdown, suitable for pasting into a PR or CI log. + +Each row in the verification checks table is one of: + +| Status | Meaning | +| --- | --- | +| `PASS` | Rule fired, evidence satisfies the assertion | +| `FAIL` | Rule fired, evidence violates the assertion — **release-blocking** | +| `WARN` | Rule fired, evidence is borderline | +| `INFO` | Informational signal, not an assertion | +| `SKIP` | Rule could not run — reason surfaced (e.g. "evidence field missing") | +| `MANUAL` | Requires dashboard verification — surfaced at the top of the report | +| `DISABLED` | Rule is in the pack but explicitly turned off | +| `N/A` | Rule doesn't apply to this platform / SDK version | + +A single `FAIL` blocks the release. `MANUAL` items don't block automatically but appear at the top so you can verify them quickly in the dashboard. + +If mobile-mcp is installed and screenshots are enabled in your rule pack, the report's "Test environment" block also embeds an end-of-smoke screenshot (proof the harness was reachable) and, on Phase 4c timeouts, a diagnostic screenshot of whatever was on screen when polling gave up. + +### Two run modes + +| Mode | When | What changes | +| --- | --- | --- | +| **synthetic** (default) | Pre-release SDK upgrade verification | The skill drives the harness, audits the synthetic occurrence | +| **prod canary** (explicit `--mode=prod-canary`) | Day-1 of a staged rollout | Skips the smoke; audits real-user production traffic from the new SDK. Adds an SDK-version regression diff across `app_versions`, `oses`, `devices`, `current_views`, `app_status`, `experiments`. PII findings are release-blocking. | + +The skill refuses to audit production traffic without the explicit mode flag. + +--- + +## File map + +``` +plugins/luciq/ +├── commands/ +│ └── luciq-verify.md ← /luciq-verify slash command (invokes this skill) +└── skills/ + └── luciq-verify/ + ├── README.md ← you are here (human-facing) + ├── SKILL.md ← LLM-facing instructions; the workflow definition + └── references/ + ├── payload-schemas.md ← MCP tool surface, response shapes, wire formats for log archives + ├── check-catalog.md ← Full E/C/S/P/A/T/U rule catalog with evidence sources + ├── rule-pack-format.md ← luciq-verify.yaml schema, base pack, worked example + └── harness-contract.md ← Scaffold + reuse modes, per-platform paths, API surface, gating +``` + +The references are loaded by the skill only when the corresponding phase needs them — progressive disclosure keeps the main workflow document focused. The slash command file is a thin wrapper that maps `/luciq-verify` to invoking the skill. + +--- + +## Status + +The workflow, rule catalog, payload schemas, and harness contract are designed and verified against: + +- Real `get_occurrence_details` payloads (iOS CRASH, NON_FATAL, FATAL_UI_HANG — all share one shape) +- Real `bug_details` payload (iOS Bug — with split log archives, root-level `experiments`, integer `state_number`) +- Real fetched **bug-channel archives** (`network_log`, `user_events`, `instabug_log`) — all plain JSON, element shapes documented in `references/payload-schemas.md` +- Real fetched **crash-channel `compressed_logs`** — confirmed encoding is **base64 → zlib → JSON object** with sub-archives keyed by name (e.g. `network_log`) +- The live Luciq MCP server API (input schemas + observed response shapes) + +What's still being verified: + +- Android / Flutter / React Native occurrence payloads (only iOS verified live; structure expected to be identical, only `current_view` semantics differ per platform). +- APM tools on a real account (verified from the MCP API, not yet probed live). +- `console_log` and `user_data` bug-channel archives (empty in every sample observed so far; element shape TBD on first non-empty sample). + +When those land, the relevant "verify live" notes in the references will be replaced with concrete field paths or parsers. + +### Behavioral findings worth knowing before you write your rule pack + +Behaviors to account for when writing your rule pack: + +1. **The SDK ships its own telemetry to `api.instabug.com`** and that traffic appears in the captured network log. C7 ("no SDK self-traffic") cannot PASS unless your rule pack's `network.url_exclude_hosts` lists those hosts. The default rule pack includes `api.instabug.com` and `*.luciq.com` for this reason. +2. **The SDK auto-redacts sensitive header values to `*****`** (e.g. `Authorization: *****`). That's the SDK's own sentinel — distinct from your customer redaction token. C4 PASSes when it sees this. +3. **The SDK truncates request bodies > 10240 bytes**, replacing the body with a literal "Request body has not been logged because it exceeds the maximum size of 10240 bytes" string. Because this happens before the redaction callback runs, the audit treats such bodies as INFO (body not evaluated), not PASS or FAIL. +4. **The `IBG-APP-TOKEN` header is not auto-masked.** Like the other `IBG-*` headers, the app token (a client-side app identifier) rides on outbound SDK requests and is not redacted to `*****` by default. If you want it masked in the captured log, add `IBG-APP-TOKEN` to `redaction.sensitive_headers`. +5. **The `instabug_log` archive is your app's logs (via `Luciq.log.i/.w/.e/.v`),** not the SDK's internal log. C9 ("no SDK warn/error") scans `console_log` (or grep inside the decoded `compressed_logs`), not `instabug_log`. + +--- + +## Related skills + +- **`luciq-setup`** — first-time SDK integration. Run this before `luciq-verify` can work. +- **`luciq-migrate`** — Instabug → Luciq rename, or vN → vN+1 API transforms. Run this *before* `luciq-verify` audits the result. +- **`luciq-debug`** — production crash / hang / bug investigation. Different use case: real-user signal, not synthetic verification. diff --git a/external_plugins/luciq/skills/luciq-verify/SKILL.md b/external_plugins/luciq/skills/luciq-verify/SKILL.md new file mode 100644 index 0000000..3b85169 --- /dev/null +++ b/external_plugins/luciq/skills/luciq-verify/SKILL.md @@ -0,0 +1,417 @@ +--- +name: luciq-verify +description: Verify a Luciq SDK upgrade end to end before shipping. Confirms the customer's custom integration (URL redirection, masking / redaction callbacks, preserved headers, persona attributes, PII masking, feature flags, experiments, user steps, user attributes) still behaves correctly against the new SDK version. Use whenever the user mentions verifying an SDK upgrade, auditing a Luciq version bump, "is it safe to release", smoke-testing the new SDK, or pastes a build with a freshly bumped Luciq dependency and asks whether to release. Scaffolds a luciq-verify harness into the debug variant, drives it to produce a fresh occurrence, pulls evidence via the Luciq MCP server (APM, bug, and crash channels), applies a customer-specific rule pack against the captured payload, and renders a pass/fail HTML+Markdown report. For first-time SDK installs use luciq-setup; for the rename/upgrade transform use luciq-migrate; for production crash investigation use luciq-debug. +--- + +# Luciq SDK Upgrade Verification + +End-to-end behavioral verification of a Luciq SDK upgrade. The mechanism is **the dashboard as oracle**: drive a debug build through a deterministic smoke, let the new SDK ship telemetry, then pull that occurrence back through MCP and audit what landed against the customer's contract. The skill catches integrations broken by SDK internal changes — redaction callbacks that no longer fire, header-preservation hooks that silently dropped, attribute APIs whose keys got renamed, new auto-instrumentation that captured PII. None of those break a build. All of them ship broken if you only test "does it compile." + +The skill is **self-contained and idempotent**. The verification harness is not a published package; this skill generates it directly inside the customer's debug variant the first time it runs and reuses it forever after. First invocation does the heavy work (harness scaffold, rule-pack bootstrap, environment confirmation); every subsequent SDK upgrade is a 2-minute "press go." + +## When NOT to use this skill + +- First-time integration of Luciq into a project that has never used Luciq, use `luciq-setup`. Setup must succeed before verification can run. +- Performing the Instabug-to-Luciq rename or applying vN-to-vN+1 API transforms, use `luciq-migrate`. Verification runs **after** the migration transform, against the new build. +- Investigating a production crash, hang, or bug, use `luciq-debug`. Verification audits a synthetic smoke; debug audits real-user signal. +- General mobile QA where Luciq is not the data source. This skill is grounded in what the Luciq MCP exposes; without it, do not pretend to use it. + +If the request fits any of the above, route there and stop — running this skill on those situations produces misleading results. + +## Prerequisites + +### Hard dependencies (skill refuses to run without these) + +| Artifact | What for | If missing | +| --- | --- | --- | +| **Luciq MCP server, authenticated** | The entire audit is grounded in what the Luciq MCP exposes — `list_applications`, `list_crashes`, `list_bugs`, `list_occurrences_tokens`, `get_occurrence_details`, `bug_details`, `crash_patterns`, and `apm_*`. Without it the skill has no oracle to verify against. | STOP at Phase 3 pre-flight. Route the user to `luciq-setup` step 7 or to https://docs.luciq.ai/product-guides-and-integrations/product-guides/ai-features/luciq-mcp-server/setup-by-ide. Do not attempt static-analysis-only "verification" — it would silently pass real regressions. | +| **A debug-variant build with the new SDK + the luciq-verify harness** | Produces a deterministic occurrence to audit | Run Phase 1 below — the skill generates the harness (scaffold mode) or validates the customer's existing dev-tools surface (reuse mode). | +| **A device, simulator, or emulator** | Executes the build that produces the occurrence | Stop; ask the user to boot one. Do not spawn one without confirmation. | + +The skill itself runs locally and pulls cloud-side telemetry — but cannot synthesize an occurrence without something running the build. This is not optional. + +### Optional integrations (the skill works without these, gains capabilities with them) + +| Integration | What it adds | If missing | +| --- | --- | --- | +| **[mobile-mcp](https://github.com/mobile-next/mobile-mcp)** server | Drives the smoke by reading the device's accessibility tree and tapping buttons by label / element ID. Required for reuse mode's `invoke_via: tap_by_label` path — useful when the customer's existing dev-tools menu can't be driven by intent extras or deep-link params. Also enables diagnostic screenshots (`optional_integrations.mobile_mcp.screenshot_on_smoke_end/timeout`). | `tap_by_label` triggers degrade to `manual` (the skill prints the trigger sequence and waits for the user to tap). Screenshots are simply not captured. Pre-flight passes unless the rule pack sets `optional_integrations.mobile_mcp.enabled: force`. | + +## Reference files + +Detailed material is split out so the SKILL.md stays workflow-focused. Read the relevant reference when the workflow points to it: + +| Reference | When to read | +| --- | --- | +| `references/payload-schemas.md` | Before any runtime audit. Defines the three channels (APM / Bug / Crash), every MCP tool's response shape, identifier model, mode/platform/crash-type enums, filter naming differences. Field paths used in this SKILL.md come from here. | +| `references/check-catalog.md` | When implementing Phase 5 (runtime audit). Full E/C/S/P/A/T/U code catalog with per-channel evidence sources and the platform applicability matrix (which rules emit `N/A` on which platforms). | +| `references/static-checks-catalog.md` | When implementing Phase 2 (static audit). Full S-* code catalog: install, modules, invocation, identity, feature-flags, logging, masking, dSYM/mapping upload, build systems, privacy modifiers. | +| `references/extractors-ios.md` | When running Phase 2 on an iOS project. Per-file scan patterns + the agent-native extraction recipe (Read + Grep instructions). | +| `references/extractors-android.md` | Phase 2 on Android. Gradle Groovy + KTS coverage. | +| `references/extractors-flutter.md` | Phase 2 on Flutter. `pubspec.yaml` + Dart source patterns. | +| `references/extractors-rn.md` | Phase 2 on React Native. `package.json` + JS/TS source patterns. | +| `references/rule-pack-format.md` | When scaffolding `luciq-verify.yaml` (Phase 1c), running bootstrap inference (Phase 1d), or processing drift detection (Phase 6b). Full YAML schema, base pack, inference rules. | +| `references/harness-contract.md` | When generating the harness (Phase 1b/1c) or regenerating it on a later run. Per-platform scaffold paths, required API surface, marker convention, debug-only gating. | + +## Canonical sources of truth + +Verify SDK API signatures and platform packaging against the live integration guides — they evolve, and signatures memorized in this skill or its references can go stale: + +| Concern | Source | +| --- | --- | +| Per-platform SDK API surface (init, hooks, callbacks, masking, identification, `reportBug`) | The platform-specific guides linked in `luciq-setup`'s "Canonical sources of truth" table | +| MCP tool surface and authentication | https://docs.luciq.ai/product-guides-and-integrations/product-guides/ai-features/luciq-mcp-server/setup-by-ide | +| App tokens, slugs, and modes | Luciq MCP `list_applications` | + +## Workflow checklist + +Track every phase. Stop on any failed step rather than continuing past a broken state — a misleading "PASS" report is worse than no report. + +``` +Verification Progress: +- [ ] 0. Detect customer maturity tier (drives phase shape below) +- [ ] 1. Setup (idempotent; no-ops on second run) +- [ ] 2. Static audit (config inspection; skipped if --runtime) +- [ ] 3. Pre-flight safety checks (skipped if --static) +- [ ] 4. Smoke — drive the harness; produce an occurrence (skipped if --static) +- [ ] 5. Runtime audit (MCP pull + rule application; skipped if --static) +- [ ] 6. Report (rendered HTML/Markdown) + drift detection +``` + +## 0. Detect customer maturity tier + +The skill **degrades gracefully** by tier. Detect which tier applies before doing anything else; the rest of the workflow branches on this. Detection is purely local (lockfile reads + MCP probes). Report the detected tier explicitly — the user often doesn't know which tier they're in until you tell them. + +| Tier | Marker | What works | +| --- | --- | --- | +| T3 (full) | Upgrade-verify harness present **and** `luciq-verify.yaml` exists | Deterministic synthetic audit with customer-specific rule pack — the end state | +| T2 (harness only) | Harness scaffolded, no rule pack | Deterministic synthetic audit against base rule pack only — most `C*` and `P*` checks run, customer-specific checks (`A*` personas, custom redaction tokens) skipped | +| T1 (telemetry only) | Neither installed, but `list_crashes` or `apm_list_groups` returns ≥ 1 record in the last 30 days from the bumped SDK version | Audit the most recent organic occurrence; `S*` synthetic-marker checks become SKIP; `C0b` (recency) becomes WARN | +| T0 (empty) | No telemetry, no harness | Cannot audit. Run Phase 1 (Setup), stop, ask the user to produce one occurrence, re-invoke | + +APM channel availability is a sub-detection: APM's `filters.platform` is `ios | android` only — Flutter (DART) and React Native (JAVASCRIPT) projects have APM permanently `N/A`. Don't probe APM on those platforms; the bug + crash channels carry the full audit there. (Detail: `references/payload-schemas.md`.) + +## 1. Setup + +Idempotent. Skip any sub-step whose artifact already exists. On a clean repo, this phase produces a single PR-shaped change set scoped strictly to the debug variant. + +### 1a. Detect platform + +Reuse `luciq-setup`'s platform-detection rules verbatim (first match wins on root markers; stop on ambiguity). Verification refuses to proceed on an ambiguous workspace — guessing the platform here corrupts every downstream step. + +### 1b. Set up the harness — scaffold or reuse + +Two modes, picked by the customer's `harness.mode` in `luciq-verify.yaml`. Default is `scaffold`. Read `references/harness-contract.md` for the full spec of both. + +**Scaffold mode** (default — `harness.mode: scaffold`) +The skill generates `LuciqVerifyHarness.` directly inside the customer's debug variant. Per-platform file paths, the required API surface, the marker convention (`current_view == "LuciqVerifyHarness"`), and debug-only gating rules are in `references/harness-contract.md`. + +Why generated, not packaged: per-customer customization (which redaction tokens to fire, which personas to test) makes a single binary library a poor fit; the generated source is small (≈ 100–200 lines per platform) and can be regenerated by the skill on subsequent runs. + +Show diffs before applying. Never touch release / production source sets, manifests, Info.plists, or entry points — a release-variant harness with a public deep link is a remote-crash vector. + +**Reuse mode** (`harness.mode: reuse`) +For projects that already have a debug menu with crash / hang / bug triggers (e.g. a `DevToolsFragment` or a `CrashLab` / `HangTrigger` / `ErrorTrigger` family), the skill drives the existing surface instead of generating a parallel one. The rule pack declares the marker view, an optional deep link / activity, and a trigger map (e.g. `forceCrash: "CrashTrigger.forceUnwrapNil"`). + +Before the smoke runs, the skill enforces the reuse-mode invariants from `references/harness-contract.md`: +- `marker_view` is non-empty and has at least one prior occurrence in the dashboard +- The reused surface is gated to the debug variant (debug source set, `#if DEBUG`, or debug-only manifest entry) +- A `flushNow` mapping exists (strongly recommended; without it recency becomes a timer race) + +Unmapped triggers become no-ops in the smoke and the rules that needed them SKIP. The audit degrades gracefully — a reuse-mode setup with only `forceCrash` and `flushNow` mapped still runs E*, C0*, S1, and (via the crash channel) most of C1–C7. + +### 1c. Scaffold the rule pack + +Write `luciq-verify.yaml` at the repo root with the base pack inlined plus TODO stubs for customer-specific rules. Schema, base pack defaults, and a worked example are in `references/rule-pack-format.md`. The first run can leave all customer-specific rules commented out; bootstrap inference (Phase 1d) and drift detection (Phase 6b) fill them in over time. + +### 1d. Bootstrap rule inference (if any telemetry exists) + +If `list_crashes` returns ≥ 10 occurrences from the **baseline** (pre-upgrade) SDK version, run the inference pass described in `references/rule-pack-format.md` ("Bootstrap inference"). The skill proposes a populated rule pack draft; commit only on user confirmation. On a brand-new integration, skip and rely on drift detection over subsequent runs. + +PII regex and custom-attribute slot mappings are **never auto-inferred and committed** — the cost asymmetry of false positives vs. missing rules favors human approval. The skill may suggest; the user approves. + +## 2. Static audit + +Inspects the customer's source tree and build config without running the app. Catches integration bugs that surface at build/config time and never get caught by the runtime audit alone: SDK not installed (or pinned to the wrong version), modules disabled by code, masking off, dSYM / mapping upload not wired, redundant invocation listeners, suspicious patterns in custom logging. + +Skipped if invoked with `--runtime`. In default and `--static` modes, this phase runs after Phase 1 (Setup) and before Phase 3 (Pre-flight). The findings feed the combined report alongside runtime-audit results. + +The audit is **agent-native**: the skill instructs the agent which files to read and which patterns to look for, via per-platform extractor reference docs. No external runtimes, no scanning daemon, no installed dependencies. + +### 2a. Discover platform-relevant files + +Reuse the platform detection from Phase 1a. Based on the detected platform, point the agent at the corresponding extractor reference: + +| Platform | Reference | Files scanned | +| --- | --- | --- | +| iOS | `references/extractors-ios.md` | `Package.resolved`, `Podfile` (+ `.lock`), `Cartfile` (+ `.resolved`), `*.swift`, `*.m`, `Info.plist`, `project.pbxproj`, dSYM upload shell scripts | +| Android | `references/extractors-android.md` | `build.gradle`(`.kts`), `settings.gradle`(`.kts`), `AndroidManifest.xml`, `*.kt`, `*.java` | +| Flutter | `references/extractors-flutter.md` | `pubspec.yaml`, `*.dart` | +| React Native | `references/extractors-rn.md` — plus `extractors-ios.md` on `ios/` and `extractors-android.md` on `android/` when those subfolders exist (RN projects are hybrid; native-side Pod / Gradle integration is part of the audit) | `package.json`, `*.{js,jsx,ts,tsx}`, and the native files when applicable | + +### 2b. Run the extractors + +Each reference doc enumerates a category of static checks (`S-*` codes — distinct from the `E*` / `C*` / `P*` / `A*` codes that live in `references/check-catalog.md` for the runtime audit). The agent reads the listed files (Read + Grep), applies the documented patterns, and produces findings. Field paths and check semantics live in `references/static-checks-catalog.md`. + +Categories per platform (full per-platform spec in each extractor reference): + +- **SDK install + version detection** — pinned version, install method, mismatched debug vs. release +- **Module activation** — Bug Reporting, Crash Reporting, APM, Session Replay, NDK, Surveys, Replies, Feature Requests, OOM monitor, ANR monitor, network auto-masking +- **Invocation events** — shake / screenshot / floating-button / two-finger swipe / programmatic +- **User identification + attribute hooks** — `setUserData`, `setCustomData`, `addUserAttribute`, `trackUserSteps` +- **Feature flag API usage** — `addFeatureFlag`, `removeFeatureFlag`, `checkFeatures` +- **Custom logging + user-event logging** — `Luciq.log*`, `LCQLog.log*`, `logUserEvent` +- **Masking / privacy config** — network auto-masking, screenshot auto-masking modes, sensitive header configuration +- **dSYM / mapping upload setup** — iOS dSYM upload script presence; Android mapping upload Gradle plugin presence +- **Build system detection** — SPM / CocoaPods / Carthage on iOS; Gradle Groovy / Gradle KTS on Android; npm on RN; pub on Flutter +- **Privacy view modifiers** — iOS only (SwiftUI `.luciqPrivate()`, UIKit equivalents) + +### 2c. Privacy constraints during extraction + +Hard constraints, baked into every extractor pattern: + +1. **Never quote contiguous source regions in findings.** The agent reads files to grep for specific patterns and may cite matched API names (e.g. `Luciq.start`, `setBugReportingEnabled`) by name. Surrounding lines, full functions, or other source regions must not be reproduced in the report — findings cite file path and 1-indexed line range only. +2. **Mask all detected tokens in the report.** If the agent extracts an app token from source, the report shows the first 4 characters + length (e.g. `2c5f… [40 chars]`), never the full token. +3. **Never read screenshots, asset binaries, or compiled artifacts.** Static analysis is text-only. +4. **Never open `.env` files even when present.** Listed in findings as "present" / "absent"; contents not read. + +The agent's outputs go into the customer's local report file; nothing is uploaded. The skill never reaches a network endpoint other than the Luciq MCP server (during runtime audit, not static). + +### 2d. Findings shape + +Each finding produces one row in the static-audit section of the combined report: + +| Field | Example | +| --- | --- | +| Code | `S-INSTALL-001` (see `references/static-checks-catalog.md`) | +| Status | `PASS` / `FAIL` / `WARN` / `INFO` / `DISABLED` / `SKIP` | +| Evidence | File path + line range (1-indexed) — never the matched text itself unless it's a known-safe identifier | +| Remediation | Doc link or short hint | + +`FAIL` blocks release the same way runtime-audit `FAIL` does. `--static` mode produces a report with only the static section populated; default mode combines static + runtime findings into one report ordered by severity. + +## 3. Pre-flight safety checks + +Runs on every invocation. The point of these checks is to refuse to verify against the wrong thing — a "PASS" report from a build that's still on the old SDK, or a debug build that's mistakenly pointing at production traffic, is worse than no report. + +| Check | What it confirms | Stop condition | +| --- | --- | --- | +| New SDK version in lockfile | The build the user is about to verify actually has the new SDK | Lockfile pins the old version | +| Build variant is debug | Smoking against `*.debug` / `Debug` configuration | Active variant is release / production | +| Backend environment is non-prod | Build is pointed at `alpha` / `beta` / `staging` / `qa` / `development` backend | Build is pointed at production API | +| Dashboard mode matches build env | A staging build should produce occurrences in `mode: staging` | Build env and `mode` mismatch | +| Device / emulator available | `adb devices` shows ≥ 1, or `xcrun simctl list devices booted` returns ≥ 1 | Nothing booted / connected | +| **Luciq MCP reachable** | `list_applications` returns the user's app at the expected `slug` | Auth expired or MCP not configured — STOP. This is the hard dependency that grounds the entire audit; the skill cannot proceed. | +| mobile-mcp probe (only if `optional_integrations.mobile_mcp.enabled: force`) | mobile-mcp's tool surface responds to a probe call | Rule pack forced it; STOP with "mobile-mcp required by rule pack but not installed." On default `enabled: auto`, missing mobile-mcp is fine — `tap_by_label` triggers degrade to manual. | + +The skill refuses to proceed against a production build variant, a build pointed at a production backend, or `mode: production` on the MCP queries. These refusals are not overridable inline — the production-canary audit is a separate mode (see "Modes" below) that the user invokes explicitly. + +## 4. Smoke + +The skill drives the harness end-to-end. No manual "tap the button" handoff unless the user prefers it. + +### 4a. Install + launch + +Default path uses platform-native commands: + +| Platform | Install | Launch harness | +| --- | --- | --- | +| iOS | `xcodebuild -scheme -destination 'platform=iOS Simulator,id=' install` | `xcrun simctl openurl luciq://luciq-verify-harness` | +| Android | `./gradlew :app:installDebug` | `adb shell am start -W -a android.intent.action.VIEW -d "luciq://luciq-verify-harness"` | +| Flutter | `flutter install --debug` | platform-specific `am start` / `simctl openurl` | +| React Native | `npx react-native run-` | as above | +| KMP | run both | as above | + +Derive ``, ``, package name from the project. Stop on ambiguity. + +If mobile-mcp is available (`optional_integrations.mobile_mcp.enabled: auto` or `force`), the skill can also use it as a unified driver — its `install_app` / `launch_app` / `open_url` primitives work across iOS and Android without the per-platform command split. This is purely a convenience; both paths produce the same outcome. + +### 4b. Trigger the canonical action sequence + +Order matters: attributes are set before network traffic so the audit sees them associated with the right session. The bug report is created before the crash so the audit gets a clean bug-channel sample alongside the crash sample. + +``` +1. LuciqVerifyHarness.setTestPersona("") +2. LuciqVerifyHarness.fireNetworkBurst(n=) +3. LuciqVerifyHarness.exerciseFeatureFlags() # iterates declared flags / experiments +4. LuciqVerifyHarness.reportBugReport() # produces a bug record with SPLIT log archives — cleanest C1–C7 evidence channel after APM +5. LuciqVerifyHarness.flushNow() # synchronously ship pending telemetry — removes the timer race +6. LuciqVerifyHarness.forceCrash() # produces the auditable crash with current_view=LuciqVerifyHarness +``` + +In **scaffold mode**, the scaffolded harness UI fires these in sequence as soon as the deep link opens it — the skill just opens the link and waits. In **reuse mode**, the skill invokes each trigger using the `invoke_via` strategy declared in the rule pack (`deep_link_param` / `intent_extra` / `tap_by_label` / `manual`). `tap_by_label` requires mobile-mcp; absent mobile-mcp, it degrades to `manual` (the skill prints the trigger sequence and waits for the user to tap). See `references/harness-contract.md` for the strategy decision table. + +### 4c. Wait for the occurrence to land — three parallel channels + +Poll all three channels because the audit pulls evidence from whichever returns data. The exact polling commands per channel are in `references/payload-schemas.md`. Summary: + +- **Crash path** (the synthetic crash and its session payload): `list_crashes` → `list_occurrences_tokens` → `get_occurrence_details`. If C1–C7 must run on the crash-path fallback (APM unavailable), immediately fetch `state.logs.compressed_logs.url` — the URL is time-limited. +- **Bug path** (the synthetic bug from `reportBugReport()`): `list_bugs` → `bug_details`. Fetch any non-empty archive URLs (`network_log`, `user_events`, etc.) immediately. +- **APM path** (iOS / Android only): `apm_list_groups` → `apm_group_view` → `apm_occurrence`. + +**Filter every poll to the smoke window.** In shared dev workspaces an unfiltered list call returns lots of irrelevant rows; the harness marker narrows but still risks latching onto an older synthetic occurrence from a previous engineer's run. Pass `date_ms.gte = now - (recency_thresholds.fail_minutes × 60_000)` on `list_crashes` and `apm_list_groups` — the window matches `C0b`'s FAIL band so anything outside it would fail recency anyway. Per-channel filter args: + +- **Crash**: `list_crashes(filters: { current_views: [""], type: "CRASH", date_ms: { gte: } })`. Default `sort_by: last_occurred_at`, `direction: desc` — take the first group that matches, then page its tokens with `list_occurrences_tokens(number: )` and apply `max(states_tokens)` (Phase 4d). +- **Bug**: `list_bugs(filters: { app_version: [] })`. `list_bugs` exposes no `date_ms` or `current_views` filter; rely on the default `sort_by: reported_at`, `direction: desc`, take recent results, and client-side discard any whose `state.fields.current_view` doesn't match the marker. +- **APM**: `apm_list_groups(filters: { date_ms: { gte: }, app_version: [] })`. APM also has no `current_views` filter; the date + app_version pair plus the harness's deterministic traffic shape (e.g. `fireNetworkBurst` against a known host) make the synthetic groups identifiable. + +Poll every 5s for up to 90s per channel. If any channel lands, proceed with that evidence and SKIP missing-channel checks with a clear reason. Stop only if all three channels timeout — diagnostics on full timeout: was `flushNow()` actually called? Is the device offline? Is the dashboard `mode` correct for the build's backend? Did `forceCrash()` actually fire (check `adb logcat` / `xcrun simctl spawn ... log stream`)? + +If mobile-mcp is available and `optional_integrations.mobile_mcp.screenshot_on_smoke_timeout: true`, capture a screenshot of the device at the moment of timeout and embed it in the report — useful diagnostic for "no occurrence landed" (was the screen blank? wrong activity? crash dialog overlay?). Similarly `screenshot_on_smoke_end: true` captures a screenshot when the smoke completes successfully, as proof of "harness was reachable and the build was installed correctly." + +### 4d. Pick the right occurrence — `max(tokens)` + +`list_occurrences_tokens` (crash) and `apm_occurrence` with `selector: list` (APM) can each return multiple tokens. In shared development workspaces where multiple engineers smoke against the same workspace concurrently, the audit must verify *this build's* synthetic occurrence — not someone else's. + +The selection rule: sort the returned tokens **lexicographically descending** and take the first (max). ULIDs are time-prefixed, so `max(tokens) ≡ newest`. Aggregate-timestamp fields like `last_occurred_at` are group-level rollups that can lag ingest order; the ULID's embedded base32 timestamp is the authoritative chronology of the occurrence itself. + +``` +# pseudocode +tokens = list_occurrences_tokens(...).tokens # ordered however the API returns +selected = max(tokens) # lex-max == ULID-newest +detail = get_occurrence_details(token=selected, ...) +``` + +Bugs are addressed by integer `number`, not ULID, so this rule doesn't apply on the bug channel. Detail: `references/payload-schemas.md` ("ULID structure and `max(tokens)` selection"). + +### 4e. Verify freshness — parse the ULID timestamp + +Once the freshest occurrence is selected, verify it's *actually* fresh enough to represent this build's behavior. Parse the ULID's embedded timestamp (first 10 base32 chars, Crockford's alphabet — full recipe in `references/payload-schemas.md`) and compare against mode-dependent thresholds (`C0b` in `references/check-catalog.md`): + +| Mode | WARN if older than | FAIL if older than | +| --- | --- | --- | +| `synthetic` (default) | 5 min | 30 min | +| `prod-canary` | 12h | 24h | + +Customers running reuse-mode against an existing dev-tools surface can override per rule pack (`recency_thresholds: { warn_minutes, fail_minutes }`) — engineers often run the in-house trigger sequence minutes-to-hours before invoking the audit, so the synthetic defaults can be too tight. + +Why parse the ULID rather than read `state.fields.reported_at`: the ULID timestamp is set at occurrence creation and is what the audit identifies the record by; `reported_at` is a separate field whose precision and timezone representation can vary. Parsing the ULID is deterministic. + +## 5. Audit + +The audit runs every rule in the merged rule pack (base + customer) against the captured payload. Each rule produces exactly one row in the report with a status, evidence string, and (on failure) a remediation pointer. + +Full rule catalog with evidence sources per channel is in `references/check-catalog.md`. The status taxonomy (PASS / FAIL / WARN / INFO / SKIP / MANUAL / DISABLED / N/A) is also there. Key principles: + +- **Cite the MCP tool result that produced each piece of evidence.** No paraphrasing, no fabrication. If the evidence comes from a presigned-URL archive, cite the archive name and the parsed line range. +- **Empty evidence is never PASS.** A missing field or empty array is SKIP with reason "evidence field missing," not silent pass. Auto-passing missing data masks integration regressions — exactly the kind of failure mode this skill exists to catch. +- **`DISABLED` is not `FAIL`.** A feature turned off at the workspace level (e.g. `user_steps` disabled by dashboard policy) is intentional configuration, not a regression. Surface as `DISABLED` with the source ("workspace policy" or "rule pack"). FAILing on intentional disables produces false-positive release blocks. See `references/check-catalog.md` for detection heuristics. +- **A single FAIL blocks the release.** MANUAL items do not block automatically but appear at the top of the report. +- **Channel preference for C1–C7 / S2 / P1 / C9: APM > Bug > Crash.** APM exposes per-request structured data; the bug payload splits logs into typed archives (`network_log`, `user_events`, `instabug_log`); the crash payload bundles everything into one archive that requires disambiguating parsing. + +### Input from Phase 2 (static audit) + +When the default invocation runs both static + runtime, Phase 2's findings shape Phase 5's rule evaluation. Before evaluating each rule whose evidence depends on a specific SDK module, the runtime audit consults Phase 2's `S-MODULE-*` findings: + +- **`S-MODULE- DISABLED`** (module turned off in source) → every dependent runtime rule emits `SKIP` with reason `"module disabled in source (S-MODULE- at :)"`. The cross-link makes the cause traceable without re-deriving it from the empty payload. +- **`S-MODULE- INFO`** (default-ON, no explicit toggle) → runtime rules run normally; the static finding is the static-side affirmation and the runtime finding is the behavioural confirmation. +- **`S-MODULE- FAIL`** (module expected per rule pack but pattern absent) → runtime rules still run, but the report flags the static finding at the top so the customer sees the misconfiguration before reading runtime detail. + +`--runtime`-only invocations skip this coordination because there is no Phase 2 input. `--static`-only invocations stop after Phase 2; no runtime rules to coordinate with. + +## 6. Report and drift detection + +### 6a. Render + +Two artifacts: +- `luciq-verify-report.html` — colored status pills, expandable evidence rows, network audit table, occurrences list. Format matches the customer-screenshot style. +- `luciq-verify-report.md` — same content, plain Markdown, for PR comments and CI logs. + +Both include: summary bar (counts per status); test environment block (slug, mode, app version, backend host, bundle ID, SDK version); selected occurrence block (type, number, ULID, reported timestamp, current_view); APM coverage block when available; verification checks table (every rule, status, evidence, source channel); network log audit table (full table for successful redaction; failed rows excluded with stated count); occurrences list (crash + bug IDs in the smoke window); user attributes; experiments. + +A single FAIL is highlighted at the top. MANUAL rows are also surfaced at top. + +### 6b. Drift detection (always runs) + +Compare the observed payload to the declared rule pack and produce a "Rule-pack drift" appendix proposing pack updates as a unified diff. Categories and proposal semantics are in `references/rule-pack-format.md` ("Drift detection"). The user accepts, rejects, or edits per hunk — never auto-edit `luciq-verify.yaml`. + +## Modes + +Two run modes are supported. Default is synthetic. Production canary requires explicit invocation. + +| Mode | When | What changes | +| --- | --- | --- | +| **synthetic** (default) | Pre-release SDK upgrade verification | Smoke phase runs; harness produces a fresh occurrence; recency window is 5 min; MCP `mode` is whichever non-prod value matches the build's backend | +| **prod canary** | Day-1 of a staged rollout, audit real-user traffic from the new SDK | Smoke phase is skipped; the skill audits the most recent occurrence with the new SDK version from MCP `mode: production`; `S*` rules SKIP; recency window is 24h; PII findings are **release-blocking** even without a synthetic FAIL. Adds an explicit SDK-version regression diff via `crash_patterns` across all six `pattern_key` values: `app_versions` (primary), `oses`, `devices`, `current_views`, `app_status`, `experiments`. Same APM diff via `apm_list_groups` filtered by `app_version: [, ]` and ranked by `apdex_change desc`. | + +Prod canary mode is invoked explicitly: `--mode=prod-canary`. The skill surfaces a top-banner warning in the report that this is production telemetry. The "prod backend" pre-flight refusal is inverted in this mode — prod *is* the target — but every other refusal still applies. + +## Invocation flags + +Orthogonal to audit mode. Default invocation runs every phase; flags trim scope when only part of the audit is needed. + +| Flag | Phases run | When to use | +| --- | --- | --- | +| (none — default) | 0, 1, 2, 3, 4, 5, 6 | Full audit. Static config inspection + runtime smoke + MCP-driven runtime audit, combined into one report. The intended path for SDK upgrades. | +| `--static` | 0, 1, 2, 6 | Static config inspection only. No smoke, no MCP. Useful as a precondition check before upgrade, or anytime the user wants a snapshot of "is my integration wired correctly" without driving the device. | +| `--runtime` | 0, 1, 3, 4, 5, 6 | Skip the static phase; go straight from setup to pre-flight + smoke + runtime audit. Useful when the user has already validated static config and only wants the upgrade-emission audit. | + +Combine with audit mode as needed: `luciq-verify --static` runs static-only synthetic mode; `luciq-verify --runtime --mode=prod-canary` skips static and audits prod telemetry. `--static --mode=prod-canary` is an error (static doesn't read telemetry, so the mode flag has nothing to apply to) — surface the conflict and stop. + +## Out of scope + +Grounded in what Luciq's MCP exposes today; the skill deliberately does not: + +- Audit metrics that are aggregate-only on the dashboard (crash-free session rate, MTTR, retention). The audit is per-occurrence behavioral, not statistical. +- Compute "is this string PII" via LLM judgment alone. Customer PII regex is the source of truth; the skill suggests candidates only. +- Run real UI tests (Espresso / XCUITest). The smoke is a single deep-link launch + canonical trigger sequence. Broader scenario coverage belongs in customer-owned UI tests that feed additional triggers into the harness, not in this skill. +- Modify the customer's actual integration code (redaction callbacks, URL rewriters, attribute setters). The skill edits the debug variant's harness scaffolding and the rule pack only. +- Verify on `mode: production` without explicit `prod-canary` invocation. + +When new MCP tools land (APM Flows, release comparison, session replay), this skill grows with them. Until then, gaps surface as `MANUAL`. + +## Style + +- Show diffs before applying any code edit (harness scaffold, harness regeneration, rule-pack updates). +- Confirm before running `pod install`, gradle syncs, build commands, or smoke triggers that install / launch the app. +- Cite the MCP tool result that produced each piece of evidence in the audit. Don't paraphrase. Don't fabricate. +- Render the report in both HTML and Markdown. +- Hold the full identifier (crash: `(slug, mode, number, ulid)`; bug: `(slug, mode, number)`; APM: `(slug, mode, metric, group_uuid|group_url, token)`) end-to-end. Partial identifiers cross-contaminate. +- Verify SDK API signatures against the live guides. Verify response field shapes against an actual response when the references say "verify live." +- Refuse production-backend audits except in explicit `prod-canary` mode. + +## Red Flags — patterns that mean STOP and surface to the user + +These are the failure modes that produce a misleading "PASS" report. If you catch yourself reasoning in any of these directions, surface to the user and don't proceed. + +**Environment and pre-flight** +- "The lockfile pins the old SDK version but I assume the user already bumped it locally." Don't assume — read the lockfile. A stale lockfile means the audit verifies the wrong build. +- "Pre-flight says the build is pointing at the prod backend, but it's a debug build so it's probably fine." It isn't. A debug build hitting prod can leak real PII into the audit payload. +- "I'll query `mode: production` because the user said 'production app'." They probably mean the production build variant for testing, not real prod telemetry. Confirm; default to a non-prod `mode`. + +**Channel and identifier confusion** +- "I'll filter `list_crashes` by `platform: flutter` and got nothing — the integration must be broken." Wrong call shape. Crash filters use UPPERCASE platform values (`DART` for Flutter, `JAVASCRIPT` for RN). The lowercase form is only for `list_applications`. Re-run before concluding anything. +- "I queried `apm_group_view` with `experiments: []` and it errored." The APM filter is singular (`experiment`); the crash filter is plural (`experiments`). They are not aliases. +- "This is a Flutter project so I'll probe APM and degrade if it fails." Don't probe. APM's platform enum is `ios | android` — DART / JAVASCRIPT is permanently `N/A`. Set the channel to N/A in Phase 0. +- "I'll use the same `(slug, mode, number, ulid)` identifier for the bug." Bugs are addressed by `(slug, mode, number)` only — there is no ULID. The bug-side identifier is `state_number` (integer) inside `state.fields`. + +**Channel-precedence and data shape** +- "Both crash channel and APM channel returned data — I'll just use the crash one." Reversed preference. Prefer APM > Bug > Crash for C1–C7. APM is the richer typed source; the fallback exists for accounts without APM, not as a default. +- "`get_occurrence_details` returned no `network_log` field — I'll FAIL the network checks." The crash payload does not carry network logs inline; they live at the presigned URL `state.logs.compressed_logs.url`. Fetch + decompress + parse. If `is_empty_array: true`, SKIP with reason "no log archive captured." +- "I'll look for `experiments` under `state.logs` on the bug payload." On bugs, `experiments` is at the **root** (sibling of `state`), not nested under `state.logs`. Different shape from crash. +- "I'll fetch the `compressed_logs.url` later when I render the report." Don't defer. The URL is presigned with an `Expires=` param — fetch immediately. Late fetches return 403. +- "The `os` field shows `iOS 26.1` so I'll match it against the platform filter `IOS`." Type confusion. `state.fields.os` is a combined human-readable string, not a platform enum. + +**Empty evidence and false positives** +- "MCP returned empty for the network log — I'll mark the redaction checks PASS because nothing is there to leak." Empty evidence is never PASS. Mark SKIP with the reason and tell the user the smoke probably didn't generate network traffic. +- "The payload has no `user_steps` key at all — I'll FAIL the user-steps checks." Could be workspace policy (`user_steps` disabled by dashboard). Mark `DISABLED` with reason `"workspace policy: user_steps disabled"` rather than FAIL. Same logic for any feature whose entire payload key is absent rather than empty — absence-of-key often means "feature off at workspace level," empty-array means "feature on, no data captured this run." +- "The non-2xx response bodies are not redacted but C3b says response bodies should be redacted." C3b excludes failed responses by design — error bodies are intentionally captured for diagnostics. Re-read the rule. +- "`state.fields.user_attributes` is `{}` so the customer's integration is broken." Maybe — but `{}` could also mean the harness didn't call `setTestPersona()`, or the customer's app doesn't set user attributes for this code path. FAIL only when `attributes.user.required` is non-empty AND a required key is missing AND the harness was supposed to set it. +- "I see strings that look like emails in user steps — I'll auto-add an email regex to the PII rule pack." PII regexes require user approval. Auto-additions create permanent false alarms. +- "I'll auto-infer the custom-attribute slot mapping from observed traffic." Slot config is org-wide dashboard configuration, not telemetry. A wrong mapping creates permanent silent false positives. Prompt the user. + +**Tool-call errors** +- "APM returned a 4xx with `{error: ...}` — I'll raise and STOP." The MCP forwards 4xx and 501 as a tool response body. Inspect the JSON, mark APM-dependent checks SKIP, continue. Stop only on 5xx (raised as `StandardError`). +- "APM tools returned 'tool not found' — I'll FAIL the network checks." Missing tools is SKIP with reason "apm tools unavailable on this account," fallback to bug / crash channel. +- "`crash_patterns` returned `MCP error -32603: Internal error` — STOP and bail." It's observably flaky. Retry once. On repeated failure, mark the prod-canary regression-diff step SKIP and continue. Don't infer "no regression" from a tool failure. + +**Workflow shortcuts** +- "The harness occurrence didn't land within the poll window, but there's an older occurrence from yesterday — I'll audit that one." Recency exists so the audit verifies *this build's* behavior. Surface the timeout. +- "Drift detection found a new attribute key — I'll add it to the rule pack and commit." Never auto-commit rule-pack changes. Propose the diff; commit only on user approval. +- "The customer's rule pack disagrees with the base pack on a header name — I'll trust the base pack." Wrong. Customer overrides always win — they're authoritative for the integration. +- "Synthetic mode is failing because the user only has prod telemetry — I'll silently switch to prod canary." Mode switches are user-invoked. Recommend explicitly; wait for the flag. +- "I'll add the harness deep-link intent filter to the main `AndroidManifest.xml` because the debug merge wasn't working." The intent filter must live in `src/debug/`. A release-variant deep link is a remote-crash vector. +- "I'll generate the harness file into `src/main/` because the project has no debug source set yet." Stop, tell the user their project needs a debug source set, let them create it. The skill does not invent build-variant separation. +- "The verification report says PASS but one rule is INFO with a slightly off value — close enough." INFO is not PASS. If the rule should assert, change its status. Otherwise leave it INFO and don't claim PASS as the summary. +- "I'll only call `reportBugReport()` if the customer opts in — it might pollute their dashboard." The bug record is in staging (per pre-flight) and tagged with the harness marker — that's exactly the kind of synthetic signal the dashboard is meant to receive. Run it every smoke; document the bug number in the report for cleanup. + +Every shortcut here trades "looks verified" for "actually verified." The skill's job is to actually verify. diff --git a/external_plugins/luciq/skills/luciq-verify/references/check-catalog.md b/external_plugins/luciq/skills/luciq-verify/references/check-catalog.md new file mode 100644 index 0000000..49003f7 --- /dev/null +++ b/external_plugins/luciq/skills/luciq-verify/references/check-catalog.md @@ -0,0 +1,220 @@ +# Check Catalog + +Every rule the **runtime audit** (Phase 5) can run, the channels each can pull evidence from, and the platform applicability matrix. Codes follow the customer-derived families: **E**nvironment, **C**apture, **S**ynthetic, **P**II, **A**ttributes, **T**racer (dashboard), **U**ser flow (dashboard). + +Companion catalog for static checks (Phase 2): `references/static-checks-catalog.md` — covers SDK install + version, module activation, invocation events, identity + attributes, feature flags, logging, masking, dSYM / mapping upload, build systems, privacy view modifiers. The two catalogs use non-overlapping code families (S-* for static, E/C/S-synthetic/P/A/T/U for runtime). + +## Table of contents + +1. [Status taxonomy](#status-taxonomy) +2. [Environment (`E*`)](#environment-e) +3. [Occurrence identity (`C0*`)](#occurrence-identity-c0) +4. [Network capture (`C1`–`C7`)](#network-capture-c1c7) +5. [Feature flags / experiments (`C8*`)](#feature-flags--experiments-c8) +6. [SDK hygiene (`C9`)](#sdk-hygiene-c9) +7. [Synthetic markers (`S*`)](#synthetic-markers-s) +8. [PII (`P*`)](#pii-p) +9. [User attributes (`A*`)](#user-attributes-a) +10. [Manual dashboard checks (`T*`, `U*`)](#manual-dashboard-checks-t-u) +11. [Platform applicability matrix](#platform-applicability-matrix) +12. [Cross-occurrence sanity (optional)](#cross-occurrence-sanity-optional) + +## Status taxonomy + +Eight statuses; do not invent new ones. Match the rendered report exactly. + +| Status | Meaning | +| --- | --- | +| `PASS` | Rule fired, evidence satisfies the assertion | +| `FAIL` | Rule fired, evidence violates the assertion — release-blocking | +| `WARN` | Rule fired, evidence is borderline — release with caveat | +| `INFO` | Informational signal, not an assertion | +| `SKIP` | Rule could not run — surfaces the reason (e.g. "evidence field missing", "apm tools unavailable") | +| `MANUAL` | Rule requires human dashboard verification — never auto-PASS | +| `DISABLED` | Rule is technically applicable but the feature it tests against is intentionally off. Two sources: (a) rule pack explicitly turns the rule off, or (b) the dashboard workspace has the feature toggled off (e.g. `user_steps` disabled at workspace level). The audit surfaces *why*. | +| `N/A` | Rule does not apply to this platform / SDK version | + +A single `FAIL` blocks the release. `MANUAL` items do not block automatically but appear at the top of the report. + +**Empty evidence is never PASS.** If the audit cannot find the field path it expects, the result is SKIP with reason "evidence field missing." Marking PASS-by-default would silently mask integration regressions. + +**Distinguishing `DISABLED` from `FAIL`.** A feature toggled off at the workspace level is *intentional configuration*, not a regression. The audit reports `DISABLED` (with the source — "rule pack" or "workspace policy") and continues. If the audit instead emitted `FAIL`, every workspace that disabled `user_steps`, network logging, or any optional channel would produce a false-positive release block. + +Detection sources: +- **Rule-pack source (authoritative)**: rule pack has `disabled: true` on the rule, OR `features..workspace_disabled: true` declares a workspace-level disable the agent can't infer alone. Emit `DISABLED` with reason `"rule pack: disabled"` or `"rule pack: disabled at workspace level"`. See `rule-pack-format.md` for the `features.*.workspace_disabled` schema. +- **Inference from payload shape is unreliable**: documented archive shapes (`payload-schemas.md`) keep archive keys present with `is_empty_array: true` when no data was captured — there is no documented "key absent" state to discriminate workspace-policy-disabled from no-data-this-run. A single empty smoke is therefore ambiguous. When the agent observes a consistently empty archive across N ≥ 3 runs AND the rule pack does not declare the feature disabled, surface as `INFO` with reason `"feature evidence empty across N runs; if this is a workspace-policy disable, add features..workspace_disabled: true to luciq-verify.yaml"`. Never auto-promote to `DISABLED` — that requires user confirmation. + +## Environment (`E*`) + +| Code | Check | Evidence source | +| --- | --- | --- | +| `E1` | Test environment (backend host) identified | Static analysis of build config; cross-check `state.fields.bundle_id` matches `integration.bundle_ids.debug` from the rule pack | +| `E2` | App version identified, matches the version under test | `state.fields.app_version` | +| `E3` | Build variant is debug | Static analysis + `state.fields.bundle_id` ends with `.debug` (or matches rule-pack debug bundle ID) | +| `E4` | OS / device captured | `state.fields.os` (combined string like `"iOS 26.1"`) + `state.fields.device` | + +**Remediation when `E*` FAILs**: the build under audit doesn't match what the rule pack expects. Reconcile `integration.bundle_ids.debug` and `integration.expected_sdk_version` against the actual lockfile + bundle ID, or rerun against the correct variant. Never edit the rule pack to match a wrong build — that would silently disable the check forever. + +## Occurrence identity (`C0*`) + +| Code | Check | Evidence source | +| --- | --- | --- | +| `C0` | Latest occurrence selected for audit | `max(list_occurrences_tokens.states_tokens)` — lex-max equals ULID-newest because ULIDs are time-prefixed; do not assume API-returned order. Response also includes `total_occurrences`. | +| `C0b` | Selected occurrence is recent. Source: parsed ULID timestamp (first 10 base32 chars of the token, Crockford's alphabet — see `payload-schemas.md` for the recipe). Thresholds are mode-dependent and rule-pack-overridable via `recency_thresholds: { warn_minutes, fail_minutes }`. | Parsed ULID timestamp | +| `C0c` | SDK version recorded matches the version under test | `state.fields.sdk_version` (e.g. `"19.6.1"`) | +| `C0d` | State token returned matches the ULID queried | `state.fields.state_token == ` — cross-app / cross-mode sanity check | + +### `C0b` recency thresholds (defaults) + +| Mode | WARN if older than | FAIL if older than | Rationale | +| --- | --- | --- | --- | +| `synthetic` | 5 min | 30 min | Smoke just ran; freshest occurrence should be brand new. | +| `prod-canary` | 12h | 24h | Audits real-user telemetry; lenient by design. | + +Customers can override per environment via the rule pack: + +```yaml +recency_thresholds: + warn_minutes: 120 # e.g. 2h — reuse-mode workflows where engineers + fail_minutes: 1440 # smoke ahead of running the audit +``` + +Useful for reuse-mode setups that drive an existing in-house dev-tools surface — engineers often run the trigger sequence minutes-to-hours before invoking the audit, so the synthetic-mode defaults are too tight. + +**Remediation when `C0*` FAILs**: +- `C0` empty → the smoke didn't reach the dashboard. Check that `flushNow()` actually fired, the device is online, the MCP `mode` matches the build's backend, and `forceCrash()` actually crashed (`adb logcat` / `xcrun simctl spawn … log stream`). +- `C0b` FAIL (occurrence too old) → rerun the smoke, or raise `recency_thresholds` if reuse-mode workflow puts the audit hours after the trigger sequence. Do not silently widen the band to hide a stale audit. +- `C0c` mismatch → the lockfile and the installed build disagree. Clean and rebuild before re-auditing. +- `C0d` mismatch → cross-app or cross-mode contamination. Re-check the slug/mode pre-flight before continuing. + +## Network capture (`C1`–`C7`) + +Channel preference: **APM > Bug > Crash**. The skill picks the first channel that returned data. + +| Code | Check | APM (primary) | Bug (dedicated `network_log` archive) | Crash (bundled `compressed_logs` archive) | +| --- | --- | --- | --- | --- | +| `C1` | URL normalization: every captured URL matches the customer's allow-list / normalization pattern | `apm_group_view` group URL / pattern; `apm_occurrence.url` per row | URL entries in parsed `network_log` | URL entries in parsed `compressed_logs` | +| `C2` | Required custom headers present on every request | `apm_occurrence` request headers | header entries in parsed `network_log` | header entries in parsed `compressed_logs` | +| `C3a` | Request body redacted (token from rule pack) on all entries | `apm_occurrence` request body | `network_log[*].request` (field name is `request`, not `request_body`) | same field inside decoded `compressed_logs.network_log[*].request` | +| `C3b` | Response body redacted on **successful** entries only; failures exempt | `apm_occurrence` response body, filtered via `failure_type` / `failure_name` | `network_log[*].response` + `status` | same fields inside decoded `compressed_logs.network_log[*]` | +| `C4` | Sensitive headers (`Authorization`, `Cookie`, `Set-Cookie`, etc.) absent or redacted | `apm_occurrence` request + response headers | `network_log[*].headers` and `.response_headers` | same fields inside decoded `compressed_logs.network_log[*]` | +| `C5` | Attachment URL paths redacted (no opaque IDs in path segments) | `apm_group_view` group URL pattern; `apm_occurrence.url` per row | `network_log[*].url` | same field inside decoded `compressed_logs.network_log[*]` | +| `C6` | Task / trace correlation IDs captured (presence on every request) | `apm_occurrence` request headers (or `custom_attributes` if customer routes the trace ID there) | `network_log[*].headers` | same field inside decoded `compressed_logs.network_log[*]` | +| `C7` | No Luciq self-traffic captured (SDK must not surveil itself) | `apm_list_groups` — no group should have a Luciq host pattern | `network_log[*].url` filtered against an exclude list | same field inside decoded `compressed_logs.network_log[*]` | + +### Critical semantics observed live + +`C3b` excludes failed responses by design — error bodies are intentionally captured for diagnostics. On APM, filter via `failure_type` / `failure_name`. On crash / bug, filter by HTTP `status >= 400` (or `status == 0` for network errors). The report footer states the exclusion count explicitly (e.g. "16 non-2xx / failed rows excluded from response-body redaction check"). + +**Three special string values** the SDK puts into the network log that the audit must recognize separately: + +| Value | Where | Meaning | How `C3a` / `C4` should react | +| --- | --- | --- | --- | +| `"*****"` | `headers.` | SDK auto-redacted a sensitive header value | C4 PASS — the SDK did the right thing | +| `"Request body has not been logged because it exceeds the maximum size of 10240 bytes"` | `request` field | SDK truncated the body before the customer's redaction callback ran | C3a INFO (not PASS) — body bypassed customer redaction; raise visibility but don't FAIL | +| `` (e.g. `""`) | `request` / `response` field | Customer's redaction callback ran and replaced the body | C3a / C3b PASS | + +**`C7` (no SDK self-traffic) needs an exclude list**: outbound Luciq SDK requests to `api.instabug.com/api/sdk/v3/*` DO appear in the captured network log — the SDK does not self-filter. The customer's rule pack should specify `network.url_exclude_hosts` (e.g. `["api.instabug.com", "*.luciq.com"]`) for C7 to evaluate cleanly. Without an exclude list, C7 effectively can't PASS on a build that emits any SDK telemetry. + +**`IBG-*` headers are not auto-masked**: outbound SDK requests carry `IBG-APP-TOKEN` (a client-side app identifier), `IBG-CUUID`, `IBG-OS-VERSION`, etc. These are not redacted to `*****` by default. If the customer wants the app token masked in the captured log, they add `IBG-APP-TOKEN` to `redaction.sensitive_headers` in the rule pack. + +**Remediation when `C1`–`C7` FAIL**: +- `C1` URL drift → the customer's URL normalization callback no longer fires, or the new SDK has a renamed hook. Compare the customer's integration code against the SDK's current URL-rewrite API in the platform integration guide. +- `C2` missing required header → the header-injection callback regressed. Confirm the hook installs on the network stack the SDK uses now (a SDK upgrade may have switched from URLSession/OkHttp to a wrapper). +- `C3a` / `C3b` / `C4` redaction failures → the redaction callback signature may have changed across SDK versions; verify against the live integration guide. Report the count of rows that bypassed redaction (e.g. `3/107`). +- `C5` opaque IDs in attachment URLs → the attachment-URL rewriter regressed in the new SDK. Patch the rewrite hook. +- `C7` self-traffic captured → add SDK telemetry hosts to `network.url_exclude_hosts` in the rule pack (`api.instabug.com`, `*.luciq.com`). + +## Feature flags / experiments (`C8*`) + +| Code | Check | Evidence source | +| --- | --- | --- | +| `C8` | Feature flags / experiments logged (count > 0) | Bug path: root `experiments` value (object or `null`). Crash path: `state.logs.experiments` — if `is_empty_array: false`, fetch + parse the presigned `url`. APM path: `apm_group_view.views[].pattern_key: experiment` (dimensions view). | +| `C8b` | Flag / experiment key length within SDK truncation limit | Per-flag key length from whichever response carries it | + +## SDK hygiene (`C9`) + +| Code | Check | Evidence source | +| --- | --- | --- | +| `C9` | No Luciq SDK `warn` or `error` lines in the app log over the smoke window | Bug path: `state.logs.console_log.url` (device console log captured by the SDK, when configured) — grep for lines tagged with the Luciq logger prefix at level `w` or `e`. Crash path: scan inside decoded `compressed_logs` for SDK-tagged lines. **Do NOT use `instabug_log` for this** — that archive carries the customer's own `Luciq.log.X()` calls, not SDK-internal messages. | + +**Remediation when `C8*` or `C9` FAIL**: +- `C8` empty → flag delivery may be gated (network state, sampling, feature flag config). The flag API may also have a new signature in the bumped SDK — verify against the integration guide. +- `C8b` key truncated → key length exceeds the SDK's truncation limit; rename the flag key to fit. +- `C9` SDK warn/error line found → quote the message and search the SDK changelog. Usually points to a missing config: dSYM upload not wired, mode mismatch, missing entitlement, deprecated API call. The report should embed the offending lines verbatim. + +## Synthetic markers (`S*`) + +These confirm the smoke actually ran. They SKIP in tier T1 (telemetry-only mode). + +| Code | Check | Evidence source | +| --- | --- | --- | +| `S1` | Harness marker present (`current_view == "LuciqVerifyHarness"`) | `state.fields.current_view` | +| `S2` | User steps / breadcrumbs captured at expected threshold | Bug path: `state.logs.user_events.url` — dedicated breadcrumbs archive. Crash path: inside `state.logs.compressed_logs` archive (fetch + parse). SKIP if archive `is_empty_array: true` | + +**Remediation when `S*` fail**: +- `S1` missing → the harness Activity / View Controller isn't surfacing as `current_view` at the moment `forceCrash()` fires. Confirm the harness screen is foregrounded; in reuse mode confirm `harness.reused_surface.marker_view` matches what the screen actually surfaces. +- `S2` empty → either user_steps is disabled at the workspace level (declare via `features.user_steps.workspace_disabled: true`) OR the SDK didn't capture steps during the smoke (the harness may need a few `Luciq.logUserEvent(...)` calls to make S2 deterministic). + +## PII (`P*`) + +PII regexes come from the rule pack. The skill must not invent them — false positives ("`name` is PII") are worse than the missing rule. + +| Code | Check | Evidence source | +| --- | --- | --- | +| `P1` | PII regex scan over user steps clean | Bug path: `state.logs.user_events.url` (cleanest — dedicated archive). Crash path: text content of `state.logs.compressed_logs` archive; SKIP if `is_empty_array: true` | +| `P2` | PII regex scan over attribute values clean | `state.fields.user_attributes` (crash + bug) + bug path's `state.logs.user_data.url` + APM `user_attributes` / `custom_attributes` blocks | +| `P3` | PII regex scan over URL query strings clean | APM (primary); bug path's `network_log.url` (mid); crash path's `compressed_logs` (fallback) | +| `P4` | PII regex scan over identity fields | `state.user.email`, `state.user.name`, `state.fields.email`, `state.fields.user_name`. On bug payload, top-level `email` field also exists — scan that too. Empty values pass trivially; non-empty must not match regex | + +**Remediation when `P*` FAIL**: every PII finding needs human review before release — the regex matched a known pattern but the matched string might be legitimate (e.g. a test email seeded by the harness). Quote the matched string (masked: first 3 chars + `***`) and the source field path. If confirmed PII, the customer's masking callback isn't covering this code path; extend it for the affected scope and re-verify. **Never** silently widen the PII regex to ignore the leak. + +## User attributes (`A*`) + +APM splits attributes into two buckets with different shapes — the rule pack and the audit must respect this: + +- **`user_attributes`**: identity-tier, **named** key/value pairs (tenant, locale, install source, persona). On APM's filter surface, `user_attributes` is an object keyed by attribute name. The rule pack enumerates required *names*. +- **`custom_attributes`**: feature-tier, attached to specific operations. On APM, custom attributes are addressed by **numbered slot 1–20** (`custom_attribute_1` ... `custom_attribute_20` per `apm_group_view.views[].pattern_key`). The dashboard maps each slot to a logical name; the customer's rule pack supplies that mapping so the audit can reference attributes by name. + +Codes are templated per customer; the rule pack lists required keys / slots. + +| Code | Check | Evidence source | +| --- | --- | --- | +| `A1`–`An` (user) | Required `user_attributes[]` present per `attributes.user.required` | `state.fields.user_attributes` (object keyed by name; `{}` when none set) — crash path. APM path: `apm_occurrence` user_attributes block. | +| `Ax1`–`Axn` (custom) | Required `custom_attribute_` populated per `attributes.custom.required_slots` (with the slot→name mapping from the rule pack used for human-readable reporting) | APM only — `apm_occurrence` custom_attributes block. Crash payload does NOT carry custom_attributes inline; if APM is N/A or unavailable on the account, all `Ax*` rules SKIP with reason "custom attributes only visible via APM channel." | +| `Ay` | All attributes (both buckets) scanned for PII | Both blocks against `pii.regex` | + +### Custom-attribute slot mapping discipline + +The slot→name mapping is **organization-wide dashboard configuration**, not per-build. The skill cannot infer it from telemetry alone. On first run, prompt the user to populate `attributes.custom.slot_map` in the rule pack; absent that, the audit emits SKIP with reason "custom-attribute slot mapping not configured" for every `Ax*` rule. Never guess a mapping — a wrong mapping creates permanent silent false positives. + +**Remediation when `A*` FAIL**: +- Required user attribute missing → either the harness's `setTestPersona()` didn't run, OR the SDK's `setUserAttribute` API signature changed in the bumped version. Check the integration guide first, then the harness trigger sequence. +- `Ax*` SKIPs from missing slot map → populate `attributes.custom.slot_map` in `luciq-verify.yaml`. The mapping is dashboard config; ask the dashboard admin or read it from the org's Luciq settings. +- `Ay` PII match in attributes → same handling as `P*`: review, extend the customer's masking before re-verifying. + +## Manual dashboard checks (`T*`, `U*`) + +These never auto-PASS. The report links them out to dashboard URLs and waits for human verification. + +| Code | Check | What the user does | +| --- | --- | --- | +| `T1` | Task-ID / hostname tracer reaches dashboard | Open the network dashboard for the test app, confirm trace correlated | +| `U1` | User flow / flow attribute renders in APM Flows | Open the APM Flows view, confirm flow tagged correctly (note: APM `metric: flows` may not be GA yet — this is a placeholder for the upcoming metric) | + +**Remediation for `T*` / `U*`**: dashboard verification only — the skill can't auto-verify. Follow the dashboard URL in the report row and confirm visually. If absent, the runtime audit's structured findings (C-family) usually explain the cause; investigate those first before assuming a dashboard issue. + +## Platform applicability matrix + +Some rules do not apply on every platform. The audit emits `N/A` (not SKIP, not PASS) for rules that don't apply. + +| Platform | `ANR` rules | `OOM` rules | APM channel | `current_view` semantics | +| --- | --- | --- | --- | --- | +| iOS (`IOS`) | N/A (no `ANR` type; iOS UI hangs via `list_app_hangs`) | Applicable | Eligible — probe to confirm | Top-most `UIViewController` class name | +| Android (`ANDROID`) | Applicable | Applicable (treated as `CRASH` until exposed otherwise) | Eligible — probe to confirm | Top-most `Activity` / `Fragment` | +| Flutter (`DART`) | Applicable | Applicable | **N/A permanently** — do not probe | Route name or widget | +| React Native (`JAVASCRIPT`) | Applicable | Applicable | **N/A permanently** — do not probe | Screen name or navigator route | + +## Cross-occurrence sanity (optional) + +If `list_occurrences_tokens` returns multiple recent occurrences from the smoke, the audit may also assert consistency: the customer's persona attribute should appear with the **same value** across all of them. Drift across the smoke session is itself a finding — surface as WARN or INFO depending on severity. diff --git a/external_plugins/luciq/skills/luciq-verify/references/extractors-android.md b/external_plugins/luciq/skills/luciq-verify/references/extractors-android.md new file mode 100644 index 0000000..dbf8ea7 --- /dev/null +++ b/external_plugins/luciq/skills/luciq-verify/references/extractors-android.md @@ -0,0 +1,206 @@ +# Android Extractors + +Per-file scan recipe for Android projects in Phase 2 (static audit). Covers both Gradle Groovy and Gradle KTS variants. + +## Table of contents + +1. [Scan plan](#scan-plan) +2. [Gradle scripts](#gradle-scripts) +3. [`AndroidManifest.xml`](#androidmanifestxml) +4. [Source files (`*.kt`, `*.java`)](#source-files-kt-java) +5. [Mapping upload pipeline](#mapping-upload-pipeline) +6. [Anti-patterns to flag](#anti-patterns-to-flag) + +## Scan plan + +| File | Role | +| --- | --- | +| `build.gradle`, `build.gradle.kts` (root + module-level) | Dependencies, plugins, build types | +| `settings.gradle`, `settings.gradle.kts` | Repository declarations | +| `app/build.gradle`(`.kts`) | App-module dependencies + plugin application | +| `**/AndroidManifest.xml` | Permissions, activities, application class | +| `**/*.kt` | Kotlin source (init, modules, masking, identity, flags, logging) | +| `**/*.java` | Java source (same surface) | + +Skip directories: `.git/`, `build/`, `.gradle/`, `.cxx/`, `release/`, `*.aar/`. Bytes-only files (`.so`, `.png`, etc.) are never read. + +## Gradle scripts + +### Dependency detection (`S-INSTALL-001`, `S-INSTALL-002`) + +Search for the Luciq Maven coordinate in any `*.gradle*` file: + +- Current: `ai.luciq.library:luciq` (and submodules `:luciq-core`, `:luciq-bug`, `:luciq-crash`, `:luciq-apm`, `:luciq-survey`) +- Legacy: `com.instabug.library:instabug` (and submodules) + +Match forms: +- Groovy: `implementation 'ai.luciq.library:luciq:X.Y.Z'` +- KTS: `implementation("ai.luciq.library:luciq:X.Y.Z")` +- Version catalog reference: `implementation(libs.luciq)` — cross-check against `gradle/libs.versions.toml` if present + +Emits: +- `S-BUILD-AND-GROOVY` and/or `S-BUILD-AND-KTS PASS` based on which file types are present +- `S-INSTALL-001 PASS` if a Luciq coord is found in any Gradle file +- `S-INSTALL-002` per `expected_sdk_version` cross-check +- `S-INSTALL-004 WARN` if both `ai.luciq.*` AND `com.instabug.*` coords coexist — message: `"both Luciq and legacy Instabug Maven coords declared; if you're mid-migration, run luciq-migrate to finish the rename. Long-term coexistence is not supported."` + +### Mapping upload plugin (`S-SYMBOL-AND-*`) + +Search for the Luciq Gradle plugin: + +- Groovy: `apply plugin: 'luciq.upload'`, `apply plugin: 'instabug.upload'` (legacy) +- KTS: `id("luciq.upload")`, `id("instabug.upload")` inside the `plugins { }` block + +If plugin applied, look for the configuration block: + +```groovy +luciqUpload { + applicationToken = "..." + // mappingFileUploadPath, etc. +} +``` + +Emits: +- `S-SYMBOL-AND-PLUGIN PASS` if plugin applied +- `S-SYMBOL-AND-TOKEN PASS` if a token configuration block is found; `WARN` if plugin applied but no token block +- Mask the detected token in the report (first 4 chars + length, never the full token) + +## `AndroidManifest.xml` + +Read every manifest in the project (main + variant-specific). + +### Permissions + +| Manifest entry | Maps to | +| --- | --- | +| `android.permission.INTERNET` | required for any SDK to ship telemetry — `S-INSTALL-* INFO` if missing (extremely unusual) | +| `android.permission.ACCESS_NETWORK_STATE` | informational | +| `android.permission.RECORD_AUDIO` | maps to `voice_note` attachment capability | +| `android.permission.READ_EXTERNAL_STORAGE` | gallery / photos | +| `android.permission.WRITE_EXTERNAL_STORAGE` | gallery / photos | +| `android.permission.POST_NOTIFICATIONS` | needed for in-app notifications on API 33+ | + +Cross-reference with detected attachment types from source. + +### Custom `Application` class + +If a custom `android:name="..."` is declared on the `` tag, that's where the SDK init typically lives. Note the class name so the source scan can find init there. + +## Source files (`*.kt`, `*.java`) + +### SDK init (`S-INSTALL-003`) + +Patterns: + +- `new Luciq.Builder(this, "...")` (Java) +- `Luciq.Builder(this, "...")` (Kotlin) +- `.build()` chained call closes the builder + +Multi-line builder chains are common; agent must read enough context lines to capture the chain (look for `.build()` after the `Builder()` call). Note the chained method names — they reveal initial module states and invocation events. + +### Module toggles (`S-MODULE-*`) + +| Module | Patterns | +| --- | --- | +| Bug Reporting | `BugReporting.setState`, `Luciq.setBugReportingState`, `BugReporting.setEnabled` | +| Crash Reporting | `CrashReporting.setState`, `Luciq.setCrashReportingState`, `LuciqNonFatalException` (presence implies CR is in use) | +| APM | `APM.setEnabled`, `APM.setState`, `Luciq.setApmEnabled` | +| Session Replay | `SessionReplay.setState`, `SessionReplay.setSyncCallback`, `SessionReplay.setNetworkLogsState` | +| Network Logs | `NetworkLogger.disable`, `NetworkLogger.enable`, `Luciq.setNetworkLoggingState` | +| ANR Monitor | `Luciq.setAnrMonitorEnabled`, `CrashReporting.setAnrState` | +| NDK | `LuciqNDK.init`, `setNdkCrashesEnabled` | +| Surveys / Replies / Feature Requests | `Surveys.setState`, `Replies.setState`, `FeatureRequests.setState` | +| Network Auto-Masking | `Luciq.setNetworkAutoMaskingState` | + +Builder-chained equivalents (called inside `Luciq.Builder()...build()`): +- `setBugReportingState(Feature.State.ENABLED)` +- `setCrashReportingState(Feature.State.DISABLED)` +- `setReproStepsState(State.ENABLED)` + +Capture the `Feature.State.*` value passed on the same line. + +### Invocation events (`S-INVOKE-*`) + +Patterns: +- `setInvocationEvents(` +- `LuciqInvocationEvent.SHAKE`, `LuciqInvocationEvent.SCREENSHOT`, `LuciqInvocationEvent.TWO_FINGER_SWIPE_LEFT`, `LuciqInvocationEvent.FLOATING_BUTTON`, `LuciqInvocationEvent.NONE` + +Programmatic invocation: +- `Luciq.show()`, `BugReporting.show()`, `BugReporting.invoke()` + +### Identity + attributes (`S-IDENTITY-*`) + +| Code | Patterns | +| --- | --- | +| `S-IDENTITY-USER` | `Luciq.identifyUser`, `Luciq.setUserData` | +| `S-IDENTITY-LOGOUT` | `Luciq.logoutUser`, `Luciq.logOutUser` | +| `S-IDENTITY-ATTR` | `Luciq.setUserAttribute`, `Luciq.removeUserAttribute` | +| `S-IDENTITY-CDATA` | `Luciq.setUserData(` | + +### Feature flags (`S-FLAG-*`) + +| Code | Patterns | +| --- | --- | +| `S-FLAG-ADD` | `Luciq.addFeatureFlag`, `Luciq.addFeatureFlags` | +| `S-FLAG-REMOVE` | `Luciq.removeFeatureFlag`, `Luciq.removeFeatureFlags` | +| `S-FLAG-CLEAR` | `Luciq.removeAllFeatureFlags`, `Luciq.clearAllFeatureFlags` | +| `S-FLAG-CHECK` | `Luciq.checkFeatures` | + +### Custom logging (`S-LOG-*`) + +| Code | Patterns | +| --- | --- | +| `S-LOG-API` | `Luciq.log(`, `Luciq.logVerbose(`, `Luciq.logInfo(`, `Luciq.logWarn(`, `Luciq.logError(`, `Luciq.logDebug(` | +| `S-LOG-USEREVENT` | `Luciq.logUserEvent(` | + +### Masking config (`S-MASK-*`) + +| Code | Patterns | +| --- | --- | +| `S-MASK-NETWORK` | `Luciq.setNetworkAutoMaskingState`, `Luciq.setNetworkAutoMaskingType` | +| `S-MASK-SCREEN` | `MaskingType.MEDIA`, `MaskingType.LABELS`, `MaskingType.MEDIA_AND_LABELS`, `MaskingType.NONE`, `setScreenshotMaskingEnabled` | +| `S-MASK-CALLBACK` | `setNetworkLogListener`, `setNetworkLogSyncCallback` | + +### Network interceptor presence (`S-MODULE-NLG` / `S-MASK-CALLBACK`) + +Search for known interceptor classes: + +- `LuciqOkhttpInterceptor` +- `LuciqAPMOkhttpInterceptor` +- `LuciqAPMGrpcInterceptor` + +If any are referenced, surface as `INFO` — the customer is intercepting traffic explicitly (rather than relying on auto-instrumentation). Useful diagnostic when runtime audit's C1-C7 returns unexpected coverage. + +### APM call sites (informational) + +The presence of these calls implies which APM features the customer relies on: + +- Flows: `APM.startFlow`, `APM.endFlow`, `APM.setFlowAttribute` +- Screen loading: `APM.startScreenLoading`, `APM.endScreenLoading` +- UI traces: `APM.startUITrace`, `APM.endUITrace` +- WebView: `APM.setWebViewsTrackingEnabled` + +Emit `INFO` rows under `S-MODULE-APM` for each detected capability. + +## Mapping upload pipeline + +Cross-reference Gradle (plugin applied + token configured) with the ProGuard / R8 config: + +- `proguard-rules.pro` / `consumer-rules.pro` files should not strip Luciq classes +- Look for `-keep class ai.luciq.**` rules; absence is `INFO` (Luciq libraries are usually self-protected, but custom ProGuard configs can break this) + +Emits: +- `S-SYMBOL-AND-PLUGIN PASS` if Luciq upload plugin is applied +- `S-SYMBOL-AND-TOKEN PASS` if token block is configured +- `WARN` if plugin applied but token missing +- `INFO` if no Luciq keep rules in ProGuard config (likely fine, but worth surfacing) + +## Anti-patterns to flag + +| Anti-pattern | Detection | Status | +| --- | --- | --- | +| Builder called more than once | Multiple `Luciq.Builder(` sites in source | `WARN` | +| Builder gated by a debug-only block (e.g. `BuildConfig.DEBUG`) | Init nested inside `if (BuildConfig.DEBUG) { ... }` | `FAIL` (unless project is explicitly debug-only) | +| Token in source (vs. resource / env) | Builder call with a long string literal token argument | `WARN` "credential detected — confirm scope" — masked in report | +| Both `ai.luciq.*` AND `com.instabug.*` coords | Both Maven groups present in Gradle dependencies | `WARN` — run `luciq-migrate` to finish the rename if mid-migration; long-term coexistence is unsupported | +| Module disabled in production build type | `setXState(Feature.State.DISABLED)` outside any debug-only guard | `INFO` (surface for review; intentional disables are fine) | diff --git a/external_plugins/luciq/skills/luciq-verify/references/extractors-flutter.md b/external_plugins/luciq/skills/luciq-verify/references/extractors-flutter.md new file mode 100644 index 0000000..90db6e9 --- /dev/null +++ b/external_plugins/luciq/skills/luciq-verify/references/extractors-flutter.md @@ -0,0 +1,119 @@ +# Flutter Extractors + +Per-file scan recipe for Flutter projects in Phase 2 (static audit). Static-only check; runtime audit on Flutter projects uses the bug + crash channels because APM is permanently `N/A` on Flutter (see `payload-schemas.md`). + +## Table of contents + +1. [Scan plan](#scan-plan) +2. [`pubspec.yaml`](#pubspecyaml) +3. [Dart source (`*.dart`)](#dart-source-dart) +4. [Anti-patterns to flag](#anti-patterns-to-flag) + +## Scan plan + +| File | Role | +| --- | --- | +| `pubspec.yaml` | Package declaration + pinned version | +| `pubspec.lock` | Resolved version (cross-reference) | +| `**/*.dart` | Source: init, modules, invocation, masking, identity, flags, logging | + +Skip directories: `.git/`, `build/`, `.dart_tool/`, `ios/`, `android/` (those are scanned by their respective platform extractors when the project is hybrid). + +## `pubspec.yaml` + +### Dependency detection (`S-INSTALL-001`, `S-INSTALL-002`) + +Look for the Luciq Flutter package in `dependencies:` or `dev_dependencies:`: + +```yaml +dependencies: + luciq_flutter: ^X.Y.Z +``` + +Legacy: `instabug_flutter` (during migration). + +Emits: +- `S-BUILD-FLUTTER PASS` if `pubspec.yaml` is present at scan root +- `S-INSTALL-001 PASS` if `luciq_flutter` or `instabug_flutter` declared +- `S-INSTALL-002` per rule-pack `expected_sdk_version` cross-check (parsed from `pubspec.lock` for the resolved version) +- `S-INSTALL-004 WARN` if both `luciq_flutter` and `instabug_flutter` are declared — message: `"both Luciq and legacy Instabug packages declared in pubspec.yaml; if you're mid-migration, run luciq-migrate to finish the rename. Long-term coexistence is not supported."` + +## Dart source (`*.dart`) + +### SDK init (`S-INSTALL-003`) + +Patterns: +- `Luciq.start(` +- `Luciq.start( token:` +- `await Luciq.start(` + +Look for `await` keyword on the surrounding line — Flutter init is async; missing `await` is `WARN`. + +### Module toggles (`S-MODULE-*`) + +| Module | Patterns | +| --- | --- | +| Bug Reporting | `BugReporting.setEnabled`, `BugReporting.setState`, `Luciq.setBugReportingEnabled` | +| Crash Reporting | `CrashReporting.setEnabled`, `CrashReporting.setState`, `Luciq.setCrashReportingEnabled` | +| APM | `APM.setEnabled` (subset support — APM is mostly auto-instrumented on Flutter) | +| Session Replay | `SessionReplay.setNetworkLogsEnabled`, `SessionReplay.setUserStepsEnabled`, `SessionReplay.setLuciqLogsEnabled` | +| Network Logs | `NetworkLogger.disable`, `NetworkLogger.enable` | +| Surveys | `Surveys.setEnabled` | +| Replies | `Replies.setEnabled` | +| Feature Requests | `FeatureRequests.setEnabled` | + +### Invocation events (`S-INVOKE-*`) + +Patterns: +- `setInvocationEvents:` (named arg in `Luciq.start`) +- `InvocationEvent.shake`, `InvocationEvent.screenshot`, `InvocationEvent.floatingButton`, `InvocationEvent.twoFingersSwipeLeft`, `InvocationEvent.none` + +### Identity + attributes (`S-IDENTITY-*`) + +| Code | Patterns | +| --- | --- | +| `S-IDENTITY-USER` | `Luciq.identifyUser`, `Luciq.setUserData` | +| `S-IDENTITY-LOGOUT` | `Luciq.logOut` | +| `S-IDENTITY-ATTR` | `Luciq.setUserAttribute`, `Luciq.removeUserAttribute`, `Luciq.getUserAttributeForKey` | +| `S-IDENTITY-CDATA` | `Luciq.setUserData(` | + +### Feature flags (`S-FLAG-*`) + +Flutter SDK exposes the full feature-flag API (verified): + +| Code | Patterns | +| --- | --- | +| `S-FLAG-ADD` | `Luciq.addFeatureFlag`, `Luciq.addFeatureFlags` | +| `S-FLAG-REMOVE` | `Luciq.removeFeatureFlag`, `Luciq.removeFeatureFlags` | +| `S-FLAG-CLEAR` | `Luciq.removeAllFeatureFlags`, `Luciq.clearAllFeatureFlags` | + +### Custom logging (`S-LOG-*`) + +| Code | Patterns | +| --- | --- | +| `S-LOG-API` | `Luciq.logVerbose(`, `Luciq.logInfo(`, `Luciq.logWarn(`, `Luciq.logError(`, `Luciq.logDebug(`, `LuciqLog.logVerbose(` etc. | +| `S-LOG-USEREVENT` | `Luciq.logUserEvent(` | + +### Masking config (`S-MASK-*`) + +| Code | Patterns | +| --- | --- | +| `S-MASK-SCREEN` | `setReproStepsConfig`, `setSessionsSyncCallback`, `PrivateView` widget usage | +| `S-MASK-CALLBACK` | `NetworkLogger.setObfuscateLogCallback`, `NetworkLogger.setOmitLogCallback` | + +### Route wrapping (informational) + +`MaterialApp` typically wraps with `LuciqNavigatorObserver` for screen-loading APM. If the customer uses `MaterialApp.router` (Navigator 2.0), they need the observer wired through `routerDelegate`. Detection: + +- `LuciqNavigatorObserver` referenced in `*.dart` → `INFO` +- Absence with route-based app → `INFO` "screen-loading APM may be partial without LuciqNavigatorObserver" + +## Anti-patterns to flag + +| Anti-pattern | Detection | Status | +| --- | --- | --- | +| `Luciq.start` without `await` | Init call not preceded by `await` on the same / previous line | `WARN` | +| Init in `main()` after `runApp()` | Init must precede `runApp` to capture early errors | `WARN` | +| Token in source (vs. read from env / `--dart-define`) | Long string literal as first positional arg to `Luciq.start` | `WARN` masked in report | +| Both `luciq_flutter` and `instabug_flutter` declared | Both packages in `pubspec.yaml` dependencies | `WARN` — run `luciq-migrate` to finish the rename if mid-migration; long-term coexistence is unsupported | +| Module disabled in release mode source path | `setXEnabled(false)` outside any `kDebugMode` guard | `INFO` surface for review | diff --git a/external_plugins/luciq/skills/luciq-verify/references/extractors-ios.md b/external_plugins/luciq/skills/luciq-verify/references/extractors-ios.md new file mode 100644 index 0000000..56573d0 --- /dev/null +++ b/external_plugins/luciq/skills/luciq-verify/references/extractors-ios.md @@ -0,0 +1,191 @@ +# iOS Extractors + +Per-file scan recipe for iOS projects in Phase 2 (static audit). The agent uses these patterns with Read + Grep tools; no Python, no scanning daemon. Each subsection lists the file glob, the patterns to look for, and the S-* codes the patterns produce. + +## Table of contents + +1. [Scan plan](#scan-plan) +2. [Package manifests](#package-manifests) +3. [Source files (`*.swift`, `*.m`)](#source-files-swift-m) +4. [Xcode project + Info.plist](#xcode-project--infoplist) +5. [dSYM upload pipeline](#dsym-upload-pipeline) +6. [Privacy view modifiers](#privacy-view-modifiers) +7. [Anti-patterns to flag](#anti-patterns-to-flag) + +## Scan plan + +| File | Role | +| --- | --- | +| `Package.resolved` | SPM dependency lockfile (presence + Luciq version) | +| `Podfile`, `Podfile.lock` | CocoaPods declaration + lockfile | +| `Cartfile`, `Cartfile.resolved` | Carthage declaration + lockfile | +| `**/*.swift` | Source: init, modules, invocation, masking, identity, flags, logging, privacy modifiers | +| `**/*.m` | Objective-C source (init, ObjC log macros, ObjC module APIs) | +| `**/Info.plist` | Permission usage descriptions; Luciq does not require any but the presence is informational | +| `**/project.pbxproj` | Build phases (dSYM upload script invocation); `DEBUG_INFORMATION_FORMAT` for Release config | +| `luciq_dsym_upload.sh`, `upload_symbols.sh`, `upload_dsym.sh`, `instabug.sh` | Symbol upload scripts | + +Read all of the above. Skip directories: `.git/`, `Pods/Headers/`, `DerivedData/`, `build/`, `*.xcframework/`, `.build/`. Bytes-only files (images, fonts, compiled binaries) are never read. + +## Package manifests + +### `Package.resolved` (Swift Package Manager) + +Grep for `"location"` keys (case-sensitive) ending in known Luciq repository slugs: `luciq-ios-sdk`, `luciq-package` (and legacy `Instabug-SPM`). Adjacent `"version"` key holds the pinned semver. + +Emits: +- `S-BUILD-IOS-SPM PASS` (file present at repo root or any subfolder) +- `S-INSTALL-001 PASS` if Luciq location found; `FAIL` if file present but no Luciq entry +- `S-INSTALL-002` if `expected_sdk_version` is set in rule pack: compare parsed `version` to expected; `PASS` / `FAIL` / `WARN` (patch mismatch) + +### `Podfile` + `Podfile.lock` + +Grep `Podfile` for `pod 'Luciq'` (single or double quotes). Pinned-version forms: `pod 'Luciq', '~> X.Y'` / `'X.Y.Z'` / `:git => '…', :tag => 'vX.Y.Z'`. Cross-reference `Podfile.lock` for the resolved version under `PODS:` → `- Luciq (X.Y.Z)`. + +Emits: +- `S-BUILD-IOS-PODS PASS` if either file present +- `S-INSTALL-001 PASS` if `pod 'Luciq'` line found +- `S-INSTALL-002` per `expected_sdk_version` cross-check +- `WARN` if `Podfile.lock` is absent or out of sync with `Podfile` + +### `Cartfile` + `Cartfile.resolved` + +Grep `Cartfile` for `github "Instabug/Luciq"` or `git "https://…/luciq-ios-sdk"`. Resolved version lives in `Cartfile.resolved` as `vX.Y.Z`. + +Emits: +- `S-BUILD-IOS-CART PASS` if either file present +- `S-INSTALL-001 PASS` if Luciq entry found + +## Source files (`*.swift`, `*.m`) + +### SDK init (`S-INSTALL-003`) + +Patterns: +- `Luciq.start(withToken:` +- `Luciq.start(token:` (legacy / Swift 5.5+ keyword-stripped form) +- `[Luciq startWithToken:` (ObjC) + +The line containing the match is the evidence. Multiple init sites is informational unless they pass different tokens (then `WARN`). + +### Module toggles (`S-MODULE-*`) + +Each toggle pattern below. The grep should be substring-anchored (not regex) because Swift / ObjC method call syntax varies. The detected value (`true` / `false`, or the boolean expression after the `=` sign on the same line) determines PASS / DISABLED. + +| Module | Patterns (any match counts) | +| --- | --- | +| Bug Reporting | `Luciq.setBugReportingEnabled`, `BugReporting.enabled`, `BugReporting.setState`, `LCQBugReporting.enabled`, `BugReporting.promptOptionsEnabledReportTypes` | +| Crash Reporting | `Luciq.setCrashReportingEnabled`, `CrashReporting.enabled`, `CrashReporting.setState`, `LCQCrashReporting.enabled` | +| APM | `APM.enabled`, `APM.setState`, `Luciq.setAPMEnabled`, `LCQAPM.enabled` | +| Session Replay | `SessionReplay.setState`, `SessionReplay.enabled`, `Luciq.setSessionReplayEnabled`, `LCQSessionReplay.enabled` | +| Network Logs | `NetworkLogger.enabled`, `NetworkLogger.setEnabled`, `Luciq.setNetworkLogging` | +| User Steps | `Luciq.trackUserSteps`, `setUserStepsEnabled` | +| ANR Monitor | `CrashReporting.appHangEnabled`, `Luciq.setANRMonitorEnabled` | +| OOM Monitor | `CrashReporting.setOOMReportingEnabled` | +| Surveys | `Surveys.enabled`, `Luciq.setSurveysEnabled` | +| Replies | `Replies.enabled`, `Luciq.setRepliesEnabled` | +| Feature Requests | `FeatureRequests.enabled` | +| Force Restart | `CrashReporting.forceRestartEnabled` | +| Network Auto-Masking | `Luciq.setNetworkAutoMaskingState`, `NetworkLogger.setNetworkAutoMasking` | + +If a pattern is absent for a default-ON module, emit `S-MODULE- INFO` with reason `"no explicit toggle in source; assumed default-ON per integration guide — runtime audit confirms"`. Do not emit `PASS` from absence alone — static analysis can't confirm the runtime state of an unconfigured module. If a pattern is present with a `false` value, emit `DISABLED` (and note the file:line). See `static-checks-catalog.md` "Module activation" for the coordinated handoff to runtime rules. + +### Invocation events (`S-INVOKE-*`) + +Pattern (regex-friendly): `\.(shake|screenshot|floatingButton|twoFingersSwipeLeft|twoFingersSwipe|rightEdgePan|none)` adjacent to a known invocation API call (`Luciq.start(...invocationEvents:`, `Luciq.invocationEvents`). + +Emits: +- `S-INVOKE-001 PASS` if at least one non-`none` event matched +- `S-INVOKE-NONE INFO` if `.none` matched +- `S-INVOKE-PROG INFO` if `Luciq.show(`, `Luciq.invoke(`, `BugReporting.show(`, or `BugReporting.invoke(` matched + +### Identity + attributes (`S-IDENTITY-*`) + +| Code | Patterns | +| --- | --- | +| `S-IDENTITY-USER` | `Luciq.identifyUser`, `Luciq.setUserData`, `Luciq.userData =` | +| `S-IDENTITY-LOGOUT` | `Luciq.logOutUser` | +| `S-IDENTITY-ATTR` | `Luciq.addUserAttribute`, `Luciq.setUserAttribute` | +| `S-IDENTITY-CDATA` | `Luciq.setCustomData`, `Luciq.userData` (property syntax) | + +### Feature flags (`S-FLAG-*`) + +| Code | Patterns | +| --- | --- | +| `S-FLAG-ADD` | `Luciq.addFeatureFlag`, `Luciq.addFeatureFlags`, `Luciq.add(featureFlag` (Swift named-arg form) | +| `S-FLAG-REMOVE` | `Luciq.removeFeatureFlag`, `Luciq.removeFeatureFlags` | +| `S-FLAG-CLEAR` | `Luciq.removeAllFeatureFlags`, `Luciq.clearAllFeatureFlags` | +| `S-FLAG-CHECK` | `Luciq.checkFeatures` | + +### Custom logging (`S-LOG-*`) + +| Code | Patterns | +| --- | --- | +| `S-LOG-API` | Swift: `Luciq.log(`, `Luciq.logVerbose(`, `Luciq.logInfo(`, `Luciq.logWarn(`, `Luciq.logError(`, `Luciq.logDebug(`, `LCQLog.log(`, `LCQLog.logVerbose(`, `LCQLog.logInfo(`, `LCQLog.logWarn(`, `LCQLog.logError(`, `LCQLog.logDebug(`. ObjC macros: `LCQLogVerbose(`, `LCQLogInfo(`, `LCQLogWarn(`, `LCQLogError(`, `LCQLogDebug(`. | +| `S-LOG-USEREVENT` | `Luciq.logUserEvent(` | + +### Masking config (`S-MASK-*`) + +| Code | Patterns | +| --- | --- | +| `S-MASK-NETWORK` | `Luciq.setNetworkAutoMaskingState` (note the enum value passed) | +| `S-MASK-SCREEN` | `setReplaceCapturedSensitiveData`, `setScreenshotMaskingEnabled` | +| `S-MASK-CALLBACK` | `setNetworkLogRequestCompletionHandler`, `setNetworkLogResponseCompletionHandler` | + +## Xcode project + Info.plist + +### `project.pbxproj` + +Read the file as text. Scan for: +- Run-script build phases referencing the dSYM upload script name (`luciq_dsym_upload.sh` / `upload_symbols.sh` / `upload_dsym.sh` / `instabug.sh`). → `S-SYMBOL-IOS-PHASE PASS` if a phase references it. +- `DEBUG_INFORMATION_FORMAT` setting. For Release configurations: `dwarf-with-dsym` is required → `S-SYMBOL-IOS-DWARF PASS`; otherwise `WARN` or `FAIL`. + +### `Info.plist` + +Inspect for usage-description keys (informational only; Luciq doesn't require any): + +| Key | Maps to | +| --- | --- | +| `NSCameraUsageDescription` | `camera` permission | +| `NSMicrophoneUsageDescription` | `microphone` permission | +| `NSPhotoLibraryUsageDescription` | `photo_library` permission | +| `NSPhotoLibraryAddUsageDescription` | `photo_library_add` permission | + +Cross-reference with detected attachment types from source (see `ATTACHMENT_PERMISSION_MAP` semantics): +- If `voice_note` attachment is detected but `NSMicrophoneUsageDescription` is missing → `S-MASK-* INFO` "voice-note attachment detected without microphone usage description" +- Similar for `gallery_image` and photo library + +## dSYM upload pipeline + +Check for the presence of one of these scripts in the repo (anywhere): + +- `luciq_dsym_upload.sh` (current) +- `Luciq_dsym_upload.sh` (current, uppercase variant) +- `upload_symbols.sh` +- `upload_dsym.sh` +- `instabug.sh` (legacy) + +If found → `S-SYMBOL-IOS-UPLOAD PASS`. Cross-reference with `project.pbxproj` to confirm a build phase invokes it → `S-SYMBOL-IOS-PHASE PASS`. + +If the script is present but no build phase invokes it: `WARN` "dSYM upload script present but not wired into the build." + +## Privacy view modifiers + +iOS-only check. Session Replay captures the view hierarchy unless views are explicitly marked private. + +Grep `*.swift`: +- `.luciqPrivate(` (SwiftUI view modifier) → `S-PRIVACY-SWIFTUI PASS` +- `setLuciqPrivate` (UIKit equivalent) → `S-PRIVACY-UIKIT PASS` + +Absence is `INFO` (most apps don't need these unless they show sensitive content not covered by automatic masking). + +## Anti-patterns to flag + +Surface as `WARN` (or `FAIL` if egregious): + +| Anti-pattern | Detection | Status | +| --- | --- | --- | +| `Luciq.start(...)` called more than once | Multiple matches of init pattern across source files | `WARN` | +| `Luciq.start(...)` inside a `#if DEBUG` only | Init in debug-gated block; release builds won't initialize | `FAIL` (unless explicitly debug-only project) | +| Token detected directly in source (vs. read from env / config) | String literal of length ≥ 32 that looks like a Luciq token argument | `WARN` "credential detected in source — confirm scope" — report masked (first 4 chars + length) | +| Legacy `Instabug` import alongside `Luciq` | `import Instabug` plus `import LuciqSDK` in the same target | `WARN` — run `luciq-migrate` to finish the rename if mid-migration; long-term coexistence is unsupported | +| Module toggle in production code path (non-debug) | A `setXEnabled(false)` outside any `#if DEBUG` / `#if PROFILE` guard | `INFO` (intentional disables are fine — surface for review) | diff --git a/external_plugins/luciq/skills/luciq-verify/references/extractors-rn.md b/external_plugins/luciq/skills/luciq-verify/references/extractors-rn.md new file mode 100644 index 0000000..180759d --- /dev/null +++ b/external_plugins/luciq/skills/luciq-verify/references/extractors-rn.md @@ -0,0 +1,127 @@ +# React Native Extractors + +Per-file scan recipe for React Native projects in Phase 2 (static audit). Static-only check; runtime audit on RN projects uses the bug + crash channels because APM is permanently `N/A` on JAVASCRIPT (see `payload-schemas.md`). + +## Table of contents + +1. [Scan plan](#scan-plan) +2. [`package.json`](#packagejson) +3. [JS/TS source (`*.{js,jsx,ts,tsx}`)](#jsts-source-jsjstsxtsx) +4. [Native side cross-references](#native-side-cross-references) +5. [Anti-patterns to flag](#anti-patterns-to-flag) + +## Scan plan + +| File | Role | +| --- | --- | +| `package.json` | Package declaration + pinned version | +| `package-lock.json` / `yarn.lock` / `pnpm-lock.yaml` | Resolved version (cross-reference) | +| `**/*.js`, `*.jsx`, `*.ts`, `*.tsx` | Source: init, modules, invocation, masking, identity, flags, logging | +| `metro.config.js` / `babel.config.js` | Informational — bundler config | + +Skip directories: `.git/`, `node_modules/`, `ios/` (iOS extractor handles), `android/` (Android extractor handles), `build/`, `dist/`. Bytes-only files (`.png`, fonts, compiled artifacts) are never read. + +## `package.json` + +### Dependency detection (`S-INSTALL-001`, `S-INSTALL-002`) + +Look in `dependencies` for the Luciq RN package: + +- Current: `@luciq/react-native` or `luciq-react-native` +- Legacy: `instabug-reactnative` (during migration) + +Cross-reference with lockfile for the actual resolved version. + +Emits: +- `S-BUILD-RN-NPM` / `S-BUILD-RN-YARN` / `S-BUILD-RN-PNPM PASS` based on which lockfile is present +- `S-INSTALL-001 PASS` if Luciq package declared +- `S-INSTALL-002` per rule-pack `expected_sdk_version` cross-check (resolved from lockfile) +- `S-INSTALL-004 WARN` if both Luciq and legacy `instabug-reactnative` declared — message: `"both Luciq and legacy Instabug packages declared in package.json; if you're mid-migration, run luciq-migrate to finish the rename. Long-term coexistence is not supported."` + +## JS/TS source (`*.{js,jsx,ts,tsx}`) + +### SDK init (`S-INSTALL-003`) + +Patterns: +- `Luciq.start(` +- `Luciq.start({` (object-arg form) +- `import Luciq from '@luciq/react-native'` / `from 'luciq-react-native'` + +Init typically lives in `App.tsx` / `App.js` or `index.js`. Note the entry point so the init position relative to `AppRegistry.registerComponent(...)` can be checked — init must run before component registration to capture early JS errors. + +### Module toggles (`S-MODULE-*`) + +| Module | Patterns | +| --- | --- | +| Bug Reporting | `BugReporting.setEnabled`, `BugReporting.setOptions`, `Luciq.setBugReportingEnabled` | +| Crash Reporting | `CrashReporting.setEnabled`, `CrashReporting.sendJSCrash`, `CrashReporting.reportError` | +| APM | `APM.setEnabled` (subset support — RN APM is auto-instrumented for screen loading + flows) | +| Session Replay | `SessionReplay.setNetworkLogsEnabled`, `SessionReplay.setUserStepsEnabled`, `SessionReplay.setLuciqLogsEnabled` | +| Network Logger | `NetworkLogger.setEnabled`, `NetworkLogger.setRequestFilterExpression` | +| NDK | `CrashReporting.setNDKCrashesEnabled` | +| Surveys | `Surveys.setEnabled` | +| Replies | `Replies.setEnabled` | +| Feature Requests | `FeatureRequests.setEnabled` | + +### Invocation events (`S-INVOKE-*`) + +Patterns: +- `invocationEvents:` (object key in `Luciq.start({invocationEvents: [...]})`) +- `InvocationEvent.shake`, `InvocationEvent.screenshot`, `InvocationEvent.floatingButton`, `InvocationEvent.twoFingersSwipeLeft`, `InvocationEvent.none` + +### Identity + attributes (`S-IDENTITY-*`) + +| Code | Patterns | +| --- | --- | +| `S-IDENTITY-USER` | `Luciq.identifyUser`, `Luciq.setUserData` | +| `S-IDENTITY-LOGOUT` | `Luciq.logOut`, `Luciq.logoutUser` | +| `S-IDENTITY-ATTR` | `Luciq.setUserAttribute`, `Luciq.removeUserAttribute`, `Luciq.getUserAttribute` | +| `S-IDENTITY-CDATA` | `Luciq.setUserData(` | + +### Feature flags (`S-FLAG-*`) + +RN SDK exposes the full feature-flag API (verified): + +| Code | Patterns | +| --- | --- | +| `S-FLAG-ADD` | `Luciq.addFeatureFlag`, `Luciq.addFeatureFlags` | +| `S-FLAG-REMOVE` | `Luciq.removeFeatureFlag`, `Luciq.removeFeatureFlags` | +| `S-FLAG-CLEAR` | `Luciq.removeAllFeatureFlags`, `Luciq.clearAllFeatureFlags` | + +### Custom logging (`S-LOG-*`) + +| Code | Patterns | +| --- | --- | +| `S-LOG-API` | `Luciq.logVerbose(`, `Luciq.logInfo(`, `Luciq.logWarn(`, `Luciq.logError(`, `Luciq.logDebug(` | +| `S-LOG-USEREVENT` | `Luciq.logUserEvent(` | + +### Masking config (`S-MASK-*`) + +| Code | Patterns | +| --- | --- | +| `S-MASK-NETWORK` | `NetworkLogger.setRequestFilterExpression`, `NetworkLogger.setObfuscateLogCallback` | +| `S-MASK-SCREEN` | `SessionReplay.setSyncCallback` (return value controls capture) | +| `S-MASK-CALLBACK` | `NetworkLogger.setObfuscateLogCallback`, `setOmitLogCallback` | + +### Module toggle state detection + +Look for `Luciq.start({ ... initEnabled: false, ... })` and similar named args in the init object — those are the canonical way to disable a module from the get-go on RN. + +## Native side cross-references + +RN projects are hybrid. The static audit invokes the iOS extractor on the `ios/` folder and the Android extractor on the `android/` folder if those subfolders exist. RN-specific findings are merged with native findings under the same S-* codes. + +Hybrid project anti-patterns: +- iOS or Android side has Luciq init but RN side doesn't (or vice versa) — `WARN` "init found on native side but not in JS — telemetry won't capture JS errors" +- Different SDK versions across iOS / Android / JS — `WARN` per version-skew detected + +## Anti-patterns to flag + +| Anti-pattern | Detection | Status | +| --- | --- | --- | +| `Luciq.start` after `AppRegistry.registerComponent` | Init runs too late; misses early errors | `WARN` | +| `Luciq.start` inside `if (__DEV__) { ... }` only | Release bundle won't initialize | `FAIL` (unless project is explicitly debug-only) | +| Token in source | Long string literal as `token:` value | `WARN` masked in report | +| Both `@luciq/react-native` AND `instabug-reactnative` in deps | Migration coexistence | `WARN` — run `luciq-migrate` to finish the rename if mid-migration; long-term coexistence is unsupported | +| Native + JS version skew | iOS / Android / JS Luciq versions don't match | `WARN` | +| Module disabled at init in production | `bugReportingEnabled: false` etc. without env guard | `INFO` surface for review | diff --git a/external_plugins/luciq/skills/luciq-verify/references/harness-contract.md b/external_plugins/luciq/skills/luciq-verify/references/harness-contract.md new file mode 100644 index 0000000..b4baf58 --- /dev/null +++ b/external_plugins/luciq/skills/luciq-verify/references/harness-contract.md @@ -0,0 +1,311 @@ +# Harness Contract + +The luciq-verify harness is generated by this skill directly into the customer's debug variant — not installed as a published package. This file specifies what every scaffolded harness must expose, where each platform's files go, and how debug-only gating is enforced. + +## Table of contents + +1. [Two modes — scaffold or reuse](#two-modes--scaffold-or-reuse) +2. [Why the harness is generated, not packaged](#why-the-harness-is-generated-not-packaged) +3. [Required API surface](#required-api-surface) +4. [Required marker](#required-marker) +5. [Required UI](#required-ui) +6. [Required gating — debug only, every platform](#required-gating--debug-only-every-platform) +7. [Per-platform scaffold paths](#per-platform-scaffold-paths) +8. [Registration call sites](#registration-call-sites) +9. [Deep-link manifest entries](#deep-link-manifest-entries) +10. [Reuse mode — driving an existing dev-tools surface](#reuse-mode--driving-an-existing-dev-tools-surface) +11. [Regeneration policy](#regeneration-policy) + +## Two modes — scaffold or reuse + +The customer's rule pack picks one mode in `harness.mode`. Default is `scaffold`. Reuse exists because many mature apps already have a debug menu / dev-tools surface with crash, hang, and bug triggers — scaffolding a parallel `LuciqVerifyHarness` would be redundant. + +| Mode | When to use | What the skill does | +| --- | --- | --- | +| `scaffold` (default) | Project has no dev-tools screen, or the team prefers the skill to own it | Generates `LuciqVerifyHarness.` source files into the debug variant (rest of this document) | +| `reuse` | Project already has a debug menu with crash / hang / bug triggers (e.g. a `DevToolsFragment` or a `CrashLab` / `HangTrigger` / `ErrorTrigger` family) | Drives the existing surface via a trigger map declared in the rule pack. No source generation. (See "Reuse mode" below.) | + +The contract — what every smoke must produce, marker conventions, debug-only gating — is the same in both modes. Only the source of the triggers differs. + +## Why the harness is generated, not packaged + +A published `@luciq/test-kit` library was considered and rejected. Reasons: + +- Per-customer customization (which redaction tokens to fire, which headers to set, which personas to test) makes a single binary library a poor fit; every customer would patch it. +- Library publication adds a release pipeline across iOS / Android / Flutter / RN / KMP, each with platform-specific gating rules. +- The generated source is small (≈ 100–200 lines per platform). Treating it as an artifact the skill writes into each repo is cheaper to maintain than treating it as a versioned dependency. +- A skill-generated harness can update itself on subsequent skill runs (see Regeneration policy below); a packaged library requires the customer to bump versions. + +Trade-off accepted: the harness source lives in the customer's repo. That's fine — it's debug-only, well-marked, and small. + +## Required API surface + +Every generated harness must expose this API, in idiomatic platform syntax: + +``` +LuciqVerifyHarness { + register(host) # one-time wire-up at app start (debug only) + + setTestPersona(name: String) + fireNetworkBurst(n: Int) # n outbound requests through the app's real HTTP client + exerciseFeatureFlags() # iterates declared flags / experiments + reportBugReport() # programmatic bug submission via Luciq's reportBug API + # — produces a bug record with SPLIT log archives + # (network_log, user_events, instabug_log, etc.) + forceCrash() # throws inside the harness Activity / VC; produces a crash + # occurrence with unified compressed_logs archive + forceANR() # blocks main thread > ANR threshold (Android / RN / + # Flutter only — iOS has no ANR concept) + forceUIHang() # iOS-only: long synchronous main-thread block; produces + # a FATAL_UI_HANG via list_app_hangs + flushNow() # synchronously ship pending telemetry +} +``` + +Why each trigger exists: +- **`setTestPersona`** seeds `user_attributes` so `A*` rules have something to assert. +- **`fireNetworkBurst`** generates traffic the SDK should intercept, enabling C1–C7. +- **`exerciseFeatureFlags`** populates experiments so C8/C8b can run. +- **`reportBugReport`** is the cleanest channel for C1–C7 — the bug payload's `network_log` is a dedicated archive (vs. crash's bundled `compressed_logs`). +- **`forceCrash`** produces the deterministic synthetic occurrence the recency check (`C0b`) and harness marker (`S1`) rely on. +- **`forceANR` / `forceUIHang`** exist so the harness can produce non-crash signals when the customer wants to verify those paths too. They're optional in the smoke sequence. +- **`flushNow`** is non-negotiable — it removes the timer race from Phase 4c's "wait for occurrence to land." Without it, the audit's recency window becomes flaky. + +`forceANR()` is generated as a no-op on iOS; `forceUIHang()` is generated as a no-op on Android. The signatures exist on every platform for symmetry; the bodies differ. + +## Required marker + +Every occurrence produced by the harness must surface with `current_view == "LuciqVerifyHarness"` (the value the MCP exposes as the top-most screen / activity / route). + +The skill filters `list_occurrences_tokens` and `list_crashes` by `current_views: ["LuciqVerifyHarness"]` and matches this exact string. The marker is how the skill distinguishes synthetic occurrences from organic ones in tier T1 audits. + +## Required UI + +A single screen with one button per trigger. Reachable via `luciq://luciq-verify-harness` deep link in debug builds only. The host segment matches the `android:host` value in the debug-only intent filter below — keep these in sync. + +The screen exists for two reasons: +1. **Manual smoke** — a developer can install the build, deep-link in, and tap buttons in 30 seconds without writing UI tests. +2. **CI smoke** — `adb shell am start -d luciq://luciq-verify-harness` (or `xcrun simctl openurl`) gets the harness into focus before the trigger sequence fires. + +## Required gating — debug only, every platform + +The harness and its deep link must not compile into release / production builds. This is enforced per platform: + +| Platform | Gating mechanism | +| --- | --- | +| iOS | `#if DEBUG` guards around all harness source files AND the `register(...)` call. Harness files in a debug-only target or compiled with the `DEBUG` Swift flag. | +| Android | Harness files live under `app/src/debug/...` only. The deep-link intent filter is in `app/src/debug/AndroidManifest.xml`. | +| Flutter | Harness file is mounted only under `if (kDebugMode)`; no production code path references the file. | +| React Native | Harness module is gated by `if (__DEV__)`; release Metro bundler tree-shakes it. | +| KMP | Debug-only source set on both Android and iOS sides. | + +The skill verifies each guard during Phase 1c. A release-variant harness with a public deep link is a remote-crash vector — STOP if any guard is missing or weakened. + +## Per-platform scaffold paths + +The skill generates these files into the customer's debug variant on first run: + +| Platform | Location | Files generated | +| --- | --- | --- | +| iOS | `/DebugOnly/LuciqVerify/` (new group); `#if DEBUG` guards | `LuciqVerifyHarness.swift` (API + screen + triggers) | +| Android | `app/src/debug/java//luciqverify/` (debug sourceSet only) | `LuciqVerifyHarness.kt`, `LuciqVerifyHarnessActivity.kt`, debug-only `AndroidManifest.xml` deep-link intent filter | +| Flutter | `lib/luciq_verify/` plus a `kDebugMode`-gated mount | `luciq_verify_harness.dart` | +| React Native | `src/luciq-verify/` plus an `if (__DEV__)` mount; screen registered only in dev | `LuciqVerifyHarness.tsx` | +| KMP | `shared/src/debugMain/...` plus thin platform shims on Android + iOS sides | `LuciqVerifyHarness.kt` (shared) + platform shims | + +If the project layout does not have a clean debug separation (no `src/debug/` for Android, no `#if DEBUG` discipline for iOS), the skill stops and asks the user to create one rather than scaffolding into `main` and silently leaking into release. + +## Registration call sites + +The skill injects a single registration call into the debug entry point, guarded by debug-only compilation: + +```swift +// iOS — in AppDelegate or scene delegate, debug only +#if DEBUG +LuciqVerifyHarness.register(application: application) +#endif +``` + +```kotlin +// Android — in Application.onCreate, in app/src/debug only +LuciqVerifyHarness.register(this) +``` + +```dart +// Flutter — at startup +if (kDebugMode) { + LuciqVerifyHarness.register(); +} +``` + +```tsx +// React Native — at root entry +if (__DEV__) { + LuciqVerifyHarness.register(); +} +``` + +## Deep-link manifest entries + +```xml + + + + + + + + +``` + +```xml + +CFBundleURLTypes + + + CFBundleURLSchemes + luciq + + +``` + +The iOS `Info.plist` entry must live in a debug-only configuration file (not the release `Info.plist`) to keep the deep-link surface out of production. + +## Reuse mode — driving an existing dev-tools surface + +When the customer's rule pack declares `harness.mode: reuse`, the skill skips source generation entirely and drives the customer's existing surface instead. This is the right mode when the project already has a debug menu like a `DevToolsFragment` or a `CrashLab` / `HangTrigger` / `ErrorTrigger` family. + +### Rule-pack declaration + +The customer declares the reused surface in `luciq-verify.yaml`: + +```yaml +harness: + mode: reuse + reused_surface: + # The current_view value occurrences from this screen surface with. + # Find it by triggering one crash from the screen, then reading state.fields.current_view + # on the resulting occurrence. Without this the skill cannot filter occurrences. + marker_view: "DevToolsFragment" + + # Optional but recommended: a deep link / activity that opens the surface so the smoke + # can be hands-free. Without this the user must navigate manually before triggering. + deep_link: "yourapp://devtools" # one of these forms — use the one your app supports + # activity: "com.example.app.debug.DevToolsActivity" + # ios_url: "yourapp://devtools" + + # Map the canonical triggers to the customer's actual methods / functions / button identifiers. + # Any unmapped trigger becomes a no-op in the smoke sequence, and the rules that depend on + # that trigger SKIP with reason "trigger not mapped in reuse mode." + triggers: + forceCrash: "CrashTrigger.forceUnwrapNil" + forceANR: "HangTrigger.dispatchSyncOnMain" + forceUIHang: "HangTrigger.dispatchSyncOnMain" # iOS-only — same fn ok on Android, will no-op + reportBugReport: "BugTrigger.reportFromDevTools" + setTestPersona: "PersonaTrigger.setTestPersona" + fireNetworkBurst: "NetworkTrigger.runBurst" + exerciseFeatureFlags: "FlagsTrigger.exerciseAll" + flushNow: "LuciqSDK.flushNow" # the SDK API directly is fine +``` + +### Required invariants (skill enforces these before the smoke runs) + +1. **`marker_view` is non-empty and present on at least one prior occurrence in the dashboard.** The skill probes `list_crashes(filters: { current_views: [marker_view] })` during Phase 0 to confirm. If the probe returns nothing, the marker is wrong (or the surface has never produced an occurrence) — the skill STOPs and asks the user to verify. +2. **The reused surface is gated to the debug variant only.** The skill cannot statically verify every code path, but it does check: (a) the declared `activity` / class lives under a debug source set or behind a `#if DEBUG` guard; (b) the declared `deep_link` intent filter is in a debug-only manifest. If gating is unclear, the skill surfaces the gap to the user before proceeding — a release-variant crash trigger is a remote-crash vector regardless of who scaffolded it. +3. **A `flushNow` mapping is strongly recommended.** Without it, Phase 4c's "wait for occurrence to land" becomes a timer race and recency checks (`C0b`) become flaky. If no flush primitive exists, the skill warns and extends the recency window to 60s. + +### Graceful degradation when triggers are unmapped + +The smoke sequence runs only the mapped triggers. The audit's rules degrade by tier: + +| Unmapped trigger | Rules that SKIP | +| --- | --- | +| `forceCrash` | `S1` harness marker on crash channel, `C0*` occurrence identity from crash. If no crash channel evidence is available at all, falls back to whichever channel did land. | +| `reportBugReport` | All bug-channel-only checks (`C9` via `console_log` when bug-only, `S2` via `user_events` when bug-only). Other channels still cover them. | +| `setTestPersona` | `A*` user-attribute rules that expected the test persona key. Customer-defined required attributes can still PASS if the app sets them organically. | +| `fireNetworkBurst` | `C1`–`C7` and `P3` if no network traffic landed from any source. APM / bug / crash channels all need *some* traffic captured. | +| `exerciseFeatureFlags` | `C8` / `C8b` if the experiments archive ends up empty. | +| `flushNow` | None directly — but recency (`C0b`) may WARN if telemetry lands late. | + +### Driving the smoke in reuse mode + +The skill drives the smoke per platform. There are four invocation strategies, ordered by determinism: + +| Strategy | What it requires | When to use | +| --- | --- | --- | +| `deep_link_param` | App accepts a URL param like `yourapp://devtools?trigger=forceUnwrapNil` | Cleanest path; most modern dev-tools menus already do this | +| `intent_extra` (Android) | App reads a key from the launching intent (`am start ... --es trigger forceUnwrapNil`) | Android-specific; common in older dev menus | +| `tap_by_label` | mobile-mcp is installed; the trigger has a stable accessibility label or ID | Best fit for legacy dev menus that pre-date automation, no API surface to drive programmatically | +| `manual` | Fallback — the skill opens the surface and asks the user to tap each button in order | Always available; needed when none of the above apply | + +The rule pack picks per trigger: + +```yaml +triggers: + forceCrash: + method: "CrashTrigger.forceUnwrapNil" # for human-readable report and crash_cause correlation + invoke_via: "intent_extra" # one of: deep_link_param | intent_extra | tap_by_label | manual + param_name: "trigger" # used with deep_link_param / intent_extra + param_value: "forceUnwrapNil" # used with deep_link_param / intent_extra + # label: "Force Crash" # used with tap_by_label (mobile-mcp finds + taps by this label) + # element_id: "force_crash_btn" # alternative to label — accessibility ID +``` + +The shorthand string form (`forceCrash: "CrashTrigger.forceUnwrapNil"`) is equivalent to `{ method: "...", invoke_via: "manual" }`. + +#### Driving via mobile-mcp (optional) + +`tap_by_label` requires the **mobile-mcp** MCP server (https://github.com/mobile-next/mobile-mcp) to be installed and authenticated in the user's agent. mobile-mcp exposes accessibility-tree reads and tap-by-coordinate primitives. When present, the skill can drive ANY UI button in the reused surface without the customer wiring intent extras or deep-link params. + +Detection: the skill probes for mobile-mcp's `list_ui_elements` / equivalent tool name at the start of Phase 4. If found and the customer has `optional_integrations.mobile_mcp.enabled: auto` (default) or `force`, the skill uses it for any trigger with `invoke_via: tap_by_label`. If mobile-mcp is absent and `enabled: auto`, those triggers fall back to `manual`. If `enabled: force`, the skill STOPs pre-flight with a "mobile-mcp is required by your rule pack but not installed" message. + +What the skill does with mobile-mcp during smoke: +1. Open the customer's surface (deep link / activity / manual nav). +2. Call mobile-mcp to list UI elements; match each trigger's `label` or `element_id` against the tree. +3. Tap the matched element by coordinates. +4. Wait briefly, then advance to the next trigger. + +If a `label` doesn't resolve to a unique element (multiple matches, or zero), the skill stops, reports the mismatch, and asks the user to disambiguate with `element_id` or fall back to manual. + +#### Optional: diagnostic screenshots + +When mobile-mcp is present, the skill can also capture screenshots for the report. Controlled by `optional_integrations.mobile_mcp.screenshot_on_smoke_end` and `.screenshot_on_smoke_timeout`. The screenshots embed in the HTML report's "Test environment" block: + +- **End-of-smoke**: a screenshot of the harness surface after the trigger sequence completed. Proof of "the harness was reachable and the build was installed correctly." +- **Timeout**: a screenshot at the moment Phase 4c's 90s polling gave up. Useful diagnostic for "no occurrence landed" — was the screen blank? Wrong activity? Crash dialog overlay? + +These default to off — opt in per rule pack. + +### What still works without mobile-mcp + +The skill remains fully functional without mobile-mcp. Everything in `tap_by_label` falls back to `manual` (the skill prints the trigger sequence and waits for confirmation), and screenshots are simply not captured. Pre-flight does not require mobile-mcp unless the rule pack explicitly sets `enabled: force`. + +### What the report shows for reuse mode + +The verification report's "Test environment" block surfaces: + +- `Harness mode: reuse` +- `Marker view: ` +- `Triggered via: ` for each trigger that fired (e.g. `crash_cause` from the resulting occurrence often confirms this — `static CrashTrigger.forceUnwrapNil()` matches the declared mapping) + +This makes it obvious to the report reader which existing dev-tools method produced the audited occurrence, so they can correlate with the customer's own dev-tools UX. + +### When reuse mode is the wrong choice + +Stick with `scaffold` (or migrate to it later) when: + +- The existing dev-tools surface is gated by something other than build variant (user role, feature flag, debug environment variable). Build-variant gating is the simplest security model and the easiest for the skill to verify. +- The existing surface's triggers don't cleanly map onto the canonical set (e.g. `forceCrash` is actually a multi-step wizard, or `reportBug` requires user input). Scaffolded triggers are deterministic by design. +- The team is adding upgrade-verification fresh and has no opinion. Default to scaffold; switch to reuse later if it makes sense. + +## Regeneration policy + +On every skill run, the harness is checked against this contract. If a previously scaffolded harness is missing any required method, marker, or gating, the skill regenerates the file after showing a diff and getting user confirmation. Reasons regeneration may be needed: + +- Customer manually edited the harness and broke a contract method +- A new trigger was added to this contract (e.g. `reportBugReport()` is recent) +- The marker convention changed (rare) +- A new SDK API replaced a deprecated one used inside the harness (e.g. `flushNow` becomes `flush(timeout:)`) + +The skill never silently rewrites. Always diff + confirm. diff --git a/external_plugins/luciq/skills/luciq-verify/references/payload-schemas.md b/external_plugins/luciq/skills/luciq-verify/references/payload-schemas.md new file mode 100644 index 0000000..163b89c --- /dev/null +++ b/external_plugins/luciq/skills/luciq-verify/references/payload-schemas.md @@ -0,0 +1,433 @@ +# Payload Schemas + +Authoritative MCP tool surface and response shapes for the audit. Field paths and enums in this file are verified live unless noted. When a field path appears in SKILL.md as `state.fields.` or `apm_occurrence.`, it comes from this document. + +## Table of contents + +1. [Three audit channels](#three-audit-channels) +2. [The Luciq MCP tool surface](#the-luciq-mcp-tool-surface) +3. [Identifiers, modes, platforms](#identifiers-modes-platforms) +4. [Crash channel: `get_occurrence_details` shape](#crash-channel-get_occurrence_details-shape) +5. [Bug channel: `bug_details` shape](#bug-channel-bug_details-shape) +6. [APM channel: `apm_*` shapes](#apm-channel-apm_-shapes) +7. [Filter naming differences across channels](#filter-naming-differences-across-channels) +8. [Operational notes](#operational-notes) + +## Three audit channels + +The audit can run on any combination of three channels, in this preference order for C1–C7 / S2 / P1 / C9: + +``` +APM > Bug > Crash +``` + +| Channel | Tool | Best for | Cost | +| --- | --- | --- | --- | +| **APM** | `apm_*` | C1–C7 network audit (structured per-request) | Cheapest — direct JSON | +| **Bug** | `bug_details` | C1–C7, S2, P1, C9 (logs pre-split into named archives) | Medium — one fetch + parse per archive, typed | +| **Crash** | `get_occurrence_details` | Synthetic crash itself, attributes, experiments, `C0*`, `S1`, `E*`, `A*` | Heaviest — network/breadcrumbs bundled into `compressed_logs` | + +Why this preference: APM exposes per-request structured data; the bug payload splits logs into typed archives (`network_log`, `user_events`, `instabug_log`); the crash payload bundles everything into one `compressed_logs` archive that requires disambiguating parsing. Cheaper, cleaner channels first. + +## The Luciq MCP tool surface + +Verified against the live Luciq MCP server API. Tool names below are exact. + +### Crash / hang / non-fatal path + +| Tool | Purpose | +| --- | --- | +| `list_applications` | Resolve the app's `slug`, `mode`, `platform`. | +| `list_crashes` | Find recent crash groups, filterable by `current_views`, `app_versions`, `os_versions`, `devices`, `platform`, `type`, `subtype`, `date_ms`, `teams`, `status_id`. | +| `list_occurrences_tokens` | Page ULIDs within a crash group, filterable by `current_views`, `app_status`, `experiments`, `app_versions`, `os_versions`, `devices`, `date_ms`. | +| `crash_details` | Group-level metadata and a sample occurrence. | +| `crash_patterns` | Distribution by `pattern_key` (default `app_versions`). Primary for SDK-version regression diffing. | +| `get_occurrence_details` | Per-occurrence payload — crash-channel evidence. | +| `list_app_hangs` | iOS UI hangs and Android ANRs (iOS has no `ANR` crash type; ANR is Android-only). | + +### Bug path + +| Tool | Purpose | +| --- | --- | +| `list_bugs` | Find bug records, filterable by `app_version`, `priority_id`, `status_id`. | +| `bug_details` | Per-bug payload with split log archives. | + +### APM path + +| Tool | Purpose | Response shape | +| --- | --- | --- | +| `apm_list_groups` | Rank groups for the app + new SDK version (`metric: network`). Sort: `failure_rate \| latency \| apdex \| apdex_change \| occurrences \| dissat_count`. | `{ metric, groups: [{ uuid, name, key_metrics }], next_offset, total }` | +| `apm_group_view` | Per-group panels. Views: `summary \| apdex_chart \| throughput_chart \| failure_rate \| spans_table \| dimensions \| outliers`. | `{ metric, group_uuid, views: { : { data } } }` + `ignored_views` array for views not applicable to the metric. | +| `apm_occurrence` | Per-occurrence detail by `selector: worst \| by_token \| list`. | `{ metric, group_uuid, first: { token, ... } }` for `selector: worst`. | + +## Identifiers, modes, platforms + +### Identifier model + +- **Crash occurrence**: `(slug, mode, number, ulid)` — `state_token` in the response equals the queried `ulid`. +- **Bug**: `(slug, mode, number)` only — no ULID. The bug-side identifier `state.fields.state_number` is an integer, not a ULID. +- **APM occurrence**: `(slug, mode, metric, group_uuid | group_url[+method], token)`. + +Hold the full identifier end-to-end on whichever channel; partial identifiers cross-contaminate. + +### ULID structure and `max(tokens)` selection + +ULIDs are time-prefixed: the first 10 base32 characters encode the millisecond timestamp at generation, the trailing 16 are random. Two consequences: + +1. **Lexicographic order matches chronological order.** Plain string comparison gives the same ordering as sorting by generation time. +2. **`max(tokens)` is the freshest occurrence.** Always. No need to fetch each occurrence's metadata to compare timestamps. + +This matters when `list_occurrences_tokens` (crash) or `apm_occurrence` with `selector: list` (APM) returns multiple tokens — common in shared development workspaces where multiple engineers smoke against the same workspace concurrently. The selection rule: + +``` +# pseudocode +tokens = list_occurrences_tokens(...).tokens # ordered however the API returns +selected = max(tokens) # lex-max == ULID-newest +detail = get_occurrence_details(token=selected, ...) +``` + +Prefer this over aggregate-timestamp fields (`last_occurred_at`, `first_occurred_at`) — those are denormalized group-level rollups that can lag ingest order. The ULID's embedded timestamp is the authoritative chronology of the occurrence itself. + +Bugs are addressed by integer `number`, not ULID; the rule doesn't apply on the bug channel. + +### Parsing the ULID timestamp + +The first 10 base32 characters encode milliseconds since the Unix epoch. Crockford's base32 alphabet — `0123456789ABCDEFGHJKMNPQRSTVWXYZ` (no I, L, O, U) — is the canonical encoding; ULIDs are case-insensitive in practice but normalize to uppercase before parsing. + +``` +# pseudocode +CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" + +def ulid_timestamp_ms(ulid): + ms = 0 + for c in ulid[:10].upper(): + ms = ms * 32 + CROCKFORD.index(c) + return ms # milliseconds since 1970-01-01 UTC + +age_seconds = (now_ms() - ulid_timestamp_ms(token)) / 1000 +``` + +Two reasons to parse the ULID timestamp directly rather than relying on `state.fields.reported_at`: + +1. **Authoritative source.** `reported_at` is a separate field that depends on when the SDK assembled the report; the ULID is set at occurrence creation and is what the audit identifies the record by. +2. **No format ambiguity.** `reported_at` is an ISO-8601 string that varies in precision and timezone representation; ULID parsing is deterministic. + +This is the input to the recency check (`C0b` in `check-catalog.md`). + +### Mode enum (every per-app tool) + +``` +alpha | beta | staging | qa | development | production +``` + +The rule pack's `env.dashboard_mode` must be one of these literals. + +### Platform enums — three forms, no aliases + +Different tools use different cases and value names. Match each tool's form exactly; querying the wrong form silently returns nothing. + +| Where | Form | Values | +| --- | --- | --- | +| `list_applications.platform` (request + response) | lowercase | `ios \| android \| react_native \| flutter` | +| `list_crashes.filters.platform`, `list_app_hangs.filters.platform` | UPPERCASE | `IOS \| ANDROID \| DART \| JAVASCRIPT` | +| `apm_*.filters.platform` | lowercase, **iOS / Android only** | `ios \| android` (no `dart` or `javascript`) | + +This means APM is **N/A for Flutter (DART) and React Native (JAVASCRIPT) projects** — do not probe APM for these platforms. + +### Crash type enum + +`list_crashes.filters.type`: platform-specific. +- Android: `CRASH`, `ANR`, `NON_FATAL`. +- iOS: `CRASH`, `OOM`, `NON_FATAL`. +- RN / Flutter: `CRASH`, `ANR`, `OOM`, `NON_FATAL`. + +iOS UI hangs surface via `list_app_hangs`, **not** as a crash type. + +### Non-fatal subtype enum + +Only valid when `NON_FATAL` is in the type filter: `CRITICAL`, `ERROR`, `WARNING`, `INFO`. + +### Crash pattern keys + +`crash_patterns.pattern_key`: `app_versions | devices | oses | current_views | app_status | experiments` (default `app_versions`). All six are usable for regression diffing — `app_versions` is the primary, `oses`/`devices` answer "OS/device-specific?", `experiments` answers "cohort-specific?". Sort by `occurrences_count`, `last_seen`, `first_seen`. + +## Crash channel: `get_occurrence_details` shape + +Verified live against four iOS occurrences (one CRASH, one NON_FATAL, one FATAL_UI_HANG, all returning the same shape). The same payload covers CRASH, NON_FATAL, OOM, ANR (where applicable), and FATAL_UI_HANG — no type-specific branching needed. + +``` +{ + state: { + fields: { + id, # integer occurrence id (distinct from state_token) + app_version, # e.g. "1.0 (1)" + current_view, # top-level screen name, "N/A" when not set + locale, # e.g. "en-EG" + sdk_version, # e.g. "19.6.1" + density, # "@3x" + screen_size, # "402x874" + city, country, # geo + reported_at, # ISO 8601, e.g. "2026-05-12T03:29:59.000Z" + bundle_id, # e.g. "com.example.your-app.debug" + email, # user identity; "" when not set + memory, # "22.0/22.0 MB" — string, not structured + storage, # "186.341/471.482 GB" — string + duration, # "00:00:01" — HH:MM:SS string + state_token, # ULID; equals the queried ulid + user_attributes, # object keyed by name; {} when none set + user_name, # "" when not set + app_status, # "foreground" | "background" + os, # combined "iOS 26.1" — NOT split into platform+version + device, # device model string; "Simulator" on simulator + variant_token # opaque + }, + logs: { + compressed_logs: { is_empty_array: , url: }, # session log archive + experiments: { is_empty_array: , url: } # experiment list + }, + user: { email, name, uuid }, + exception_message: "" + } +} +``` + +### Critical: network log is NOT inline + +Network logs, breadcrumbs, and the Luciq SDK's own log all live **inside** the `compressed_logs` archive — they are not separate inline JSON fields as `luciq-debug`'s historical examples implied. The crash-path audit for C1–C7 / S2 / P1 / C9 cannot read fields directly; it must: + +1. Check `state.logs.compressed_logs.is_empty_array`. If `true`, treat as SKIP with reason "no log archive captured." +2. Fetch the `url` over HTTP. The URL is presigned (`?Expires=&Signature=...`); no auth header needed but the URL is **time-limited** — fetch immediately on receipt, not later. +3. Decompress (the file extension is `.txt` but the body is typically gzip or similar; verify on first fetch). +4. Parse the contents (text format, line-oriented; the exact format must be codified on first parse). + +When APM is available, use it — the crash-path fallback is materially heavier. + +### Other shape notes + +- `state.fields.user_attributes` is an object keyed by name; matches APM's `user_attributes` filter shape. +- No top-level `custom_attributes`, `user_steps`, or `breadcrumbs` field in `state.fields`. Those live inside the compressed archive or are expressed only through APM. Absence is not FAIL — it means "fetch the archive" or SKIP. +- `id` is the numeric occurrence id; `state_token` is the ULID. Distinct. +- The `os` field is a combined human-readable string ("iOS 26.1"), not a platform enum. The platform-filter values are separate (see platform enum table above). + +## Bug channel: `bug_details` shape + +Verified live. Structurally similar to the crash payload but with **three meaningful differences** worth special-casing: + +``` +{ + priority_id, status_id, tags, categories: { name, subs }, + email, number, reported_at, last_activity, title, type, team, + session_id, attachments: { attachment: [...] }, + experiments, # NULL or object at ROOT — not nested under state.logs + state: { + fields: { + ... all the same fields as crash payload ... + state_number, # integer, NOT a ULID + duration # bug-session duration + }, + logs: { + # Pre-split into typed archives — NOT unified compressed_logs + console_log: { is_empty_array, [url] }, + instabug_log: { is_empty_array, [url] }, # Luciq SDK's own internal log + user_data: { is_empty_array, [url] }, + network_log: { is_empty_array, [url] }, # DEDICATED — direct C1-C7 evidence + user_events: { is_empty_array, [url] } # breadcrumbs / user steps — S2 / P1 + } + } +} +``` + +### Three differences from the crash payload + +1. **`state.logs` is split into 5 named archives** instead of one `compressed_logs` bundle. Each can be `is_empty_array: true` (no `url`) or `false` (with presigned URL). Structurally cleaner for the audit — parsers don't need to disambiguate row types. + +2. **`experiments` lives at the root** (sibling of `state`), not nested under `state.logs`. When no experiments are attached, the value is `null` (not `{ is_empty_array: true }`). + +3. **Identifier is `state_number` (integer)**, not `state_token` (ULID). `bug_details` is addressed by `(slug, mode, number)` only; there is no fourth identifier field. + +### Per-rule mapping when on the bug channel + +| Rule | Bug-channel evidence | +| --- | --- | +| C1–C7 redaction / headers | `state.logs.network_log.url` (fetch + parse) | +| C8 experiments | root `experiments` value (object or `null`) | +| C9 SDK warn/error | `state.logs.instabug_log.url` (Luciq SDK's own log — directly addresses this rule) | +| S2 user steps | `state.logs.user_events.url` (fetch + parse) | +| P1 PII over user steps | `state.logs.user_events.url` (fetch + parse) | +| P2 PII over attributes | `state.fields.user_attributes` + `state.logs.user_data.url` | +| P4 identity PII | `state.user.email/name`, `state.fields.email/user_name`, top-level `email` | + +## APM channel: `apm_*` shapes + +### Tool surface and shared filter set + +The APM filter set (shared across `apm_list_groups`, `apm_group_view`, `apm_occurrence` with minor differences per tool): + +``` +date_ms, platform (ios | android only), +app_version, device { operator: in|not_in, values: [...] }, +os_version, country, carrier, radio, +failure_name, failure_type, +response_time_ms { gt, lt }, +request_payload_size { gte, lte }, response_payload_size { gte, lte }, +custom_attributes (object, keyed by attribute number 1..20), +experiment, latency_percentile, +user_attributes (object, keyed by name) +``` + +`apm_list_groups` additionally has: `key_metric`, `group_name`, `count`, `dissat_count`, `apdex`, `apdex_change`, `95th_percentile_ms`, `50th_percentile_ms`, `total_failure_rate`, `client_failure_rate`, `server_failure_rate`. + +### Custom attributes are NUMBERED slots 1–20 + +Critical: APM does not address custom attributes by name. The `apm_group_view.views[].pattern_key` enum includes `custom_attribute_1` through `custom_attribute_20`. The dashboard maps each slot to a logical name; the customer's rule pack supplies the slot→name mapping (`attributes.custom.slot_map`). The skill **cannot infer this mapping** from telemetry alone — slot configuration is organization-wide. + +When the slot map is empty, all `Ax*` rules SKIP with reason "custom-attribute slot mapping not configured." + +### Permission model — dynamic per metric + +Required permissions are derived from the `metric` parameter: +- `.list.view` for `apm_list_groups` +- `.details.view` for `apm_group_view` +- `.occurrence_details.view` for `apm_occurrence` + +Today only `metric: network` exists. Future metrics (e.g. `flows`) auto-wire when added. A 403 from upstream means "missing permission `..view`"; treat as SKIP. + +### Availability — TWO independent constraints + +**Account availability**: APM tools may not be GA on every account. Error semantics from the MCP layer: +- 4xx and 501 (`metric_not_implemented`) → forwarded as a tool response body (NOT raised). Inspect the JSON for `{ error: ... }`. SKIP with the reason. The skill must not `try/catch` here. +- 5xx → raises `StandardError` from the MCP layer. STOP and surface the upstream failure. + +**Platform support**: APM's `filters.platform` is `ios | android` only. DART and JAVASCRIPT projects: APM channel is permanently **N/A** (not SKIP). Don't probe. Set in Phase 0 at maturity detection. + +## Filter naming differences across channels + +The same logical concept uses different names on different tools. Match each tool's form exactly. + +| Concept | Crash filter | APM filter | +| --- | --- | --- | +| App version | `app_versions` (array) | `app_version` (array) | +| Experiment | `experiments` (array) | `experiment` (array) | +| Device | `devices` (flat array) | `device` (`{ operator, values }` object) | +| OS version | `os_versions` (array) | `os_version` (array) | +| Current view | `current_views` (array) | (not exposed in APM filter set) | + +## Operational notes + +### `crash_patterns` flakiness + +The `crash_patterns` tool occasionally returns `MCP error -32603: Internal error` (observed live during schema verification). The behavior: retry once with a small backoff; on repeated failure, mark the regression-diff step (used in prod-canary mode) as SKIP with reason "crash_patterns upstream error" rather than blocking the report. Do not infer "no regression" from a tool failure. + +### Presigned-URL freshness + +All log archive URLs (`compressed_logs`, `experiments`, `network_log`, `user_events`, `console_log`, `instabug_log`, `user_data`) are signed by CloudFront with an `Expires=` query param. Treat them as ephemeral: fetch in the same phase that received the response. Late fetches return 403 / SignatureDoesNotMatch. + +## Log-archive wire formats + +Verified live against fetched archives. The `.txt` file extension is misleading on every archive — the actual encoding differs by channel. + +### Bug-channel archives (plain JSON) + +`bug_details` archive URLs return **plain JSON arrays / objects**, no compression, served as `application/octet-stream`. Just download and `JSON.parse`. + +| Archive | Top-level type | Element shape | +| --- | --- | --- | +| `network_log` | array | `{ status, response_time, method, response_headers, request?, response?, headers, log_source, date, url }` (see "Network log entry shape" below) | +| `user_events` | array | `{ event: string, params: object, timestamp: int }` (unix ms) | +| `instabug_log` | array | `{ log_message_date: int, log_message_level: "i" \| "w" \| "e" \| "v", log_message: string }` (unix ms) | +| `console_log` | array (empty in observed samples) | TBD when a non-empty sample is available | +| `user_data` | array (empty in observed samples) | TBD when a non-empty sample is available | + +### Crash-channel archives (base64 + zlib) + +`get_occurrence_details` archive URLs return **base64-encoded zlib-compressed JSON**. The first bytes are ASCII (the base64 alphabet), starting with `eJzt` / `eJys` / similar — the base64 prefix for zlib's `0x78 0x9C` magic header. Decode pipeline: + +**Python (in-process):** + +```python +import base64, zlib, json +raw = open(downloaded_file, 'rb').read().strip() +decoded = base64.b64decode(raw) # base64 → bytes +inflated = zlib.decompress(decoded) # zlib → bytes +data = json.loads(inflated) # bytes → object +``` + +**Shell (fetch + decode in one pipe, agent-friendly):** + +```bash +# Presigned URL is single-use; download to a local file first, then decode +curl -s "$PRESIGNED_URL" -o logs.txt +cat logs.txt | tr -d '\n' | base64 -d \ + | python3 -c "import sys,zlib; sys.stdout.buffer.write(zlib.decompress(sys.stdin.buffer.read()))" \ + > logs.json +``` + +`tr -d '\n'` strips line breaks that some servers introduce in base64 payloads. The trailing `> logs.json` writes the decompressed bytes to disk as JSON, ready for `jq` / `python -m json.tool` / further analysis. + +The decompressed JSON is an **object** (not an array), with sub-archives keyed by name: + +``` +{ + "network_log": [...] # same element shape as bug-channel network_log + # other keys may appear (user_events, console_log, etc.) depending on what + # the SDK captured for this occurrence; iterate over the object keys +} +``` + +So the crash-channel `compressed_logs` is the bundled equivalent of the bug-channel's separate archives. Once decoded, the element shapes match. + +### Network log entry shape (canonical across channels) + +```jsonc +{ + "status": 200, // int; HTTP status. 0 = no response (network error / timeout) + "response_time": 35.531, // float ms + "method": "GET", // HTTP method + "url": "https://...", // full URL including query + "date": 1778956610399, // unix ms + "log_source": 1, // int enum (URLSession=1, observed) + "headers": { "Authorization": "*****" }, // REQUEST headers; SDK auto-redacts sensitive + // header values to "*****" + "response_headers": { ... }, // response headers + "request": "..." | { ... } | , // request body. Absent = none sent. + // Object = JSON-parsed body. + // Literal string "Request body has not been + // logged because it exceeds the maximum + // size of 10240 bytes" = SDK SIZE-TRUNCATED + // (NOT customer redaction) + "response": { ... } | // parsed response body +} +``` + +**Field names** in the actual payload are `request` and `response` (not `request_body` / `response_body`). Audit rules that target these fields must use the actual names. + +**SDK auto-redaction sentinel**: the Luciq SDK automatically replaces sensitive header values with `*****` before logging. So `headers.Authorization == "*****"` means "captured, redacted by SDK." A customer-defined redaction (e.g. ``) would appear in `request` / `response` bodies, NOT in headers. + +**SDK size-truncation marker**: requests with bodies > 10240 bytes have their `request` field replaced with the literal string `"Request body has not been logged because it exceeds the maximum size of 10240 bytes"`. This is NOT the customer's redaction token — it means the SDK's size limit hit BEFORE the customer's redaction callback ran. The audit must distinguish: matching this string is INFO ("body bypassed customer redaction due to size"), not PASS ("redacted") and not FAIL ("leak"). + +**SDK self-traffic in the log**: outbound requests from the Luciq SDK to its own backend (`api.instabug.com/api/sdk/v3/...`) DO appear in the captured network log. The SDK does not self-filter by default. C7's "no SDK self-traffic" rule needs the customer's rule pack to provide an exclude list (e.g. `api.instabug.com`, `*.luciq.com`), or treat presence of such hosts as either a known-state OK or a finding to surface — customer's call. + +**`IBG-*` headers are not auto-masked**: outbound SDK requests carry `IBG-APP-TOKEN`, `IBG-OS`, `IBG-SDK-VERSION`, `IBG-CUUID`, `IBG-OS-VERSION` headers. The app token (`IBG-APP-TOKEN`) is a client-side app identifier. It is not redacted to `*****` by default. If the customer wants this masked in the captured log, they add `IBG-APP-TOKEN` to `redaction.sensitive_headers`. + +### What `instabug_log` actually contains + +Despite the name, `instabug_log` is **NOT the SDK's internal warn/error log**. It carries application log lines that the customer's app wrote via the Luciq SDK's logging API: + +```swift +Luciq.log.i("user viewed cart: 3 items, $49.99") // level "i" +Luciq.log.w("retrying payment submission") // level "w" +Luciq.log.e("offline: no network reachable") // level "e" +Luciq.log.v("trace: enter computeShipping()") // level "v" +``` + +Levels are single-character: `i` (info), `w` (warn), `e` (error), `v` (verbose). + +**Implication for C9** ("no Luciq SDK warn/error in app log"): `instabug_log` is the wrong source. SDK-internal warnings (e.g. "Luciq: failed to intercept request", "Luciq: masking callback threw") would appear in `console_log` if the SDK captures console (when configured) or inside the crash-channel's bundled `compressed_logs` under a different sub-key. The audit should scan `console_log` on the bug channel, or grep for SDK-tagged lines inside the decoded `compressed_logs` on the crash channel — NOT scan `instabug_log` for `w` / `e` lines (those would catch the customer's own app warnings, which is a different concern). + +### `is_empty_array` semantics + +When an archive entry's `is_empty_array` is `true`, the entry has **no `url` field**. Always check the flag before attempting a fetch. Don't infer "data missing" from a missing `url` — `is_empty_array: true` is the documented "this archive was not produced for this occurrence" signal. + +### Bug payload root vs. crash payload nesting + +The bug payload has top-level fields the crash payload doesn't (`priority_id`, `status_id`, `categories`, `attachments`, `session_id`, root-level `experiments`). The crash payload nests almost everything under `state`. When writing channel-agnostic code, always check which channel the response came from before unpicking fields. diff --git a/external_plugins/luciq/skills/luciq-verify/references/rule-pack-format.md b/external_plugins/luciq/skills/luciq-verify/references/rule-pack-format.md new file mode 100644 index 0000000..192248f --- /dev/null +++ b/external_plugins/luciq/skills/luciq-verify/references/rule-pack-format.md @@ -0,0 +1,277 @@ +# Rule Pack Format + +The customer-specific contract the audit asserts against. Lives at the project root as `luciq-verify.yaml`. The skill merges this with a base pack (defined in this file) and runs every rule in the merged set. + +## Table of contents + +1. [How the merge works](#how-the-merge-works) +2. [Schema](#schema) +3. [Worked example](#worked-example) +4. [Base pack — what the skill ships](#base-pack--what-the-skill-ships) +5. [Bootstrap inference (first run)](#bootstrap-inference-first-run) +6. [Drift detection (every subsequent run)](#drift-detection-every-subsequent-run) +7. [Anti-patterns to avoid](#anti-patterns-to-avoid) + +## How the merge works + +The audit assembles its rule set in two steps: + +1. **Base pack** ships with the skill — sensible defaults for every code in the catalog (`E*`, `C0*`, `C1`–`C9`, `S*`, `P1`, `T1`, `U1`). +2. **Customer rule pack** (`luciq-verify.yaml`) supplies the integration-specific values (`redaction.*`, `network.*`, `attributes.*`, `pii.regex`) that turn the base rules into real assertions. + +Customer entries always win on conflict — the base pack is a starting point, not authority. An empty customer pack produces a sparse but real audit; drift detection fills it in over time. + +## Schema + +```yaml +# luciq-verify.yaml — customer rule pack + +# Identity of the customer integration. Used to derive marker names and report headers. +integration: + app_slug: "example" # MCP slug from list_applications + bundle_ids: + debug: "com.example.app.debug" + release: "com.example.app" + expected_sdk_version: "" + +# Backend / dashboard scope this verification targets. +# Allowed values match the Luciq MCP `mode` enum. +env: + backend_hosts_allow: + - "*.staging.example.com" + dashboard_mode: "staging" # alpha | beta | staging | qa | development | production + +# Harness configuration. Two modes: +# scaffold (default) — the skill generates LuciqVerifyHarness into the debug variant. +# reuse — the skill drives an existing dev-tools surface in your app. +# See references/harness-contract.md for the full spec of both modes. +harness: + mode: "scaffold" # scaffold | reuse + + # Only used when mode=reuse. Triggers can be: + # - String shorthand: "CrashTrigger.forceUnwrapNil" (defaults to invoke_via=manual) + # - Object form: see below for per-trigger invoke_via options + # invoke_via options: deep_link_param | intent_extra | tap_by_label | manual + # See references/harness-contract.md "Driving the smoke in reuse mode" for the full spec. + # reused_surface: + # marker_view: "DevToolsFragment" # current_view value on occurrences from this screen + # deep_link: "myapp://devtools" # optional — for hands-free smoke + # triggers: + # forceCrash: + # method: "CrashTrigger.forceUnwrapNil" + # invoke_via: "intent_extra" + # param_name: "trigger" + # param_value: "forceUnwrapNil" + # reportBugReport: + # method: "BugTrigger.reportFromDevTools" + # invoke_via: "tap_by_label" # requires mobile-mcp (optional, see below) + # label: "Report Bug" + # setTestPersona: "PersonaTrigger.setTestPersona" # shorthand → manual + +# Optional integrations. The skill works without any of these — they unlock hands-free +# paths and richer reporting when present. +optional_integrations: + mobile_mcp: + enabled: "auto" # auto | force | off + # auto: use if installed, fall back otherwise + # force: pre-flight STOPs if not installed + # off: never use even if installed + screenshot_on_smoke_end: false # embed end-of-smoke screenshot in report + screenshot_on_smoke_timeout: false # capture diagnostic screenshot on Phase 4c timeout + +# Custom redaction contract. +redaction: + request_body_token: "REDACTED" # what the SDK should leave in request bodies + response_body_token: "REDACTED" # what it should leave in 2xx response bodies + exclude_status: [non_2xx] # body redaction exempt on these + sensitive_headers: # must be absent or redacted + - "Authorization" + - "Cookie" + - "Set-Cookie" + - "Proxy-Authorization" + +# URL normalization / capture contract. +network: + url_allow_hosts: + - "host.example.com" + url_exclude_hosts: # C7 — hosts to EXCLUDE from "no self-traffic" + - "api.instabug.com" # SDK ships telemetry here; not customer traffic + - "*.luciq.com" # future Luciq backends + required_headers_on_all_requests: + - "X-Trace-Id" + attachment_path_redacted: true + +# Attributes — two buckets: user (named) and custom (20 numbered slots). +attributes: + user: + required: + - "tenant" + - "locale" + - "install-source" + required_one_of_pattern: + - "*-persona" # at least one persona key must be set + custom: + # Dashboard configuration maps slot index (1..20) to a logical attribute name. + # The skill cannot infer this — fill it in once per project. + slot_map: + 1: "tenant_id" + 3: "feature_cohort" + required_slots: + - 1 + - 3 + +# Feature-flag / experiment expectations. +experiments: + min_count: 1 + max_key_length: 70 + +# Per-feature flags the agent can't infer from telemetry alone. +# Declare a workspace-level disable here when a feature is off at the dashboard +# (e.g. user_steps disabled by org policy) — runtime rules dependent on it +# emit DISABLED with this as the source rather than SKIP "evidence field missing" +# (which would read as a defect). +features: + user_steps: + workspace_disabled: false # true if turned off at the dashboard + session_replay: + workspace_disabled: false + network_logging: + workspace_disabled: false + +# PII regex set. Customers extend; the skill ships sensible defaults. +pii: + regex: + email: '\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b' + phone_e164: '\+\d{7,15}' + ssn_us: '\b\d{3}-\d{2}-\d{4}\b' + scopes: + - user_steps + - user_attributes + - url_query + +# Override or disable any base rule by code. +overrides: + C6: { enabled: true, evidence_hint: "X-Trace-Id" } + T1: { dashboard_url: "https://app.luciq.ai/.../network" } + # P3: { enabled: false, reason: "App does not put data in URL query" } +``` + +## Worked example + +A worked example for a project that uses custom URL normalization, header preservation, body redaction, and persona attributes: + +```yaml +integration: + app_slug: "your-app-slug" + bundle_ids: + debug: "com.your-org.your-app.debug" + release: "com.your-org.your-app" + +env: + backend_hosts_allow: + - "api.your-app.com" + dashboard_mode: "alpha" + +# Project already has DevToolsFragment with crash triggers — reuse that +# surface instead of scaffolding a parallel LuciqVerifyHarness. +harness: + mode: "reuse" + reused_surface: + marker_view: "DevToolsFragment" + triggers: + forceCrash: "CrashTrigger.forceCrash" + reportBugReport: "BugTrigger.reportFromDevTools" + setTestPersona: "PersonaTrigger.setTestPersona" + fireNetworkBurst: "NetworkTrigger.runBurst" + flushNow: "LuciqSDK.flushNow" + +redaction: + request_body_token: "" + response_body_token: "" + exclude_status: [non_2xx] + sensitive_headers: ["Authorization", "Cookie", "Set-Cookie"] + +network: + url_allow_hosts: + - "api.your-app.com" + url_exclude_hosts: + - "api.instabug.com" + - "*.luciq.com" + required_headers_on_all_requests: + - "X-Tenant" + attachment_path_redacted: true + +attributes: + user: + required: + - "tenant" + - "your-app-locale" + - "install-source" + required_one_of_pattern: + - "your-app-*-persona" + +experiments: + min_count: 1 + max_key_length: 70 +``` + +This produces an all-green report: `C1` confirms every outgoing URL matches `api.your-app.com`, `C2` confirms `X-Tenant` is present on every request, `C3a/C3b` confirm bodies are replaced with `` (non-200 responses are intentionally excluded — see C3b semantics), `A4` confirms the declared persona attribute pattern, and so on. + +## Base pack — what the skill ships + +The base pack defines every code with sensible defaults. Customer overrides apply on top. The base pack assumes: + +- `redaction.request_body_token` defaults to `REDACTED` (generic placeholder; most customers override). +- `redaction.exclude_status` defaults to `[non_2xx]` (the C3b non-2xx exemption). +- `network.attachment_path_redacted` defaults to `true`. +- `experiments.min_count` defaults to `0` (no minimum — overridable). +- `pii.regex` defaults to a generic set (email, phone, SSN, credit-card-luhn). Customers extend with industry-specific patterns. +- `pii.scopes` defaults to `[user_steps, user_attributes, url_query]`. + +Every rule code is enabled by default. Customers disable via `overrides.: { enabled: false, reason: "" }`. + +## Bootstrap inference (first run) + +If `list_crashes` returns ≥ 10 occurrences from the **baseline** (pre-upgrade) SDK version, the skill infers a draft rule pack from observed telemetry: + +1. **Headers** — for each header name, compute the fraction of requests it appears on. Headers present on ≥ 95% of requests across ≥ 5 occurrences are candidates for `network.required_headers_on_all_requests`. The skill proposes; the user approves each. + +2. **Redaction tokens** — observe the literal string occupying request and response bodies on 2xx requests. If a single token appears in ≥ 95% of bodies, propose it as `redaction.request_body_token` / `redaction.response_body_token`. + +3. **URL hosts** — observe the host distribution. Hosts above a threshold become `url_allow_hosts` candidates. + +4. **Attribute keys** — observe the key set across occurrences. Keys present in 100% of sessions are proposed as `attributes.user.required`. Keys matching a clustering pattern (`your-app-*-persona`, `tenant-*`) are proposed as `required_one_of_pattern`. + +5. **Experiment / flag count band** — observe min/max counts across occurrences; propose `min_count` at the floor. + +6. **Static-analysis crosscheck** — grep the codebase for Luciq SDK API call sites. Discrepancies (code calls `setUserAttribute("foo", ...)` but no occurrence ever shipped key `foo`) become INFO findings. + +The skill produces a proposed `luciq-verify.yaml` and **shows it to the user before writing**. The user strikes, edits, or accepts each block. The skill writes only what the user confirmed. + +### PII regex is never auto-inferred and committed + +The skill may *suggest* regexes ("I see strings matching email format in user steps — should this be flagged as PII?") but each one requires explicit user approval. Auto-adding a wrong PII rule produces false alarms forever; a missing PII rule is a known gap. The cost asymmetry favors human approval. + +### Custom-attribute slot map is never inferred + +Slot configuration is organization-wide dashboard config, not telemetry-derivable. Even if slot 1 always contains a tenant ID across observed occurrences, that doesn't make the mapping "tenant_id" canonical for the org. Prompt the user; don't guess. + +## Drift detection (every subsequent run) + +After each verification, the skill diffs observed-vs-declared and proposes pack updates as a unified diff. Categories: + +- **Newly observed** — a key/header/host that appeared this run but isn't in the pack. +- **No longer observed** — a declared rule that hasn't fired in N runs (default 5). Propose `DISABLED` or removal. +- **Drifted band** — a numeric expectation outside the declared range (e.g. flag count now 156 vs. declared `min_count: 100`). +- **Static-analysis crosscheck** — a new SDK API call site in the codebase since last run (e.g. customer just added a new redaction callback at `NetworkModule.kt:142` — propose extending the redaction rule). + +The user accepts, rejects, or edits per hunk. The skill never auto-edits `luciq-verify.yaml`. + +## Anti-patterns to avoid + +- **Trusting the base pack over the customer pack on conflict.** Customer overrides always win — they're authoritative for the integration; the base pack is a starting point. +- **Auto-committing rule-pack changes from drift detection.** Propose the diff; commit only on user approval. Drift can be intentional (a new attribute the customer just added) or accidental (a regression). The user owns that decision. +- **Inferring PII regexes from observed strings.** False positives are sticky — they generate noise every run forever. Suggest only; commit only on approval. +- **Inferring custom-attribute slot mappings.** Slot config is org-wide; the skill has no way to verify the mapping is correct. Always prompt. +- **Disabling a rule because it FAILed once.** A FAIL is a signal, not a nuisance. Investigate; don't silence. If a rule genuinely doesn't apply (e.g. a header the app doesn't use), document the reason and DISABLE explicitly. +- **Treating an empty customer pack as broken.** An empty pack runs the base rules — that's fine for a first run. The bootstrap inference step fills it in proactively when telemetry is available. diff --git a/external_plugins/luciq/skills/luciq-verify/references/static-checks-catalog.md b/external_plugins/luciq/skills/luciq-verify/references/static-checks-catalog.md new file mode 100644 index 0000000..44b8066 --- /dev/null +++ b/external_plugins/luciq/skills/luciq-verify/references/static-checks-catalog.md @@ -0,0 +1,165 @@ +# Static Checks Catalog + +The S-* code catalog Phase 2 (static audit) uses. Companion to `check-catalog.md` (runtime audit E/C/P/A codes). Every check declared here is a finding the agent emits during Phase 2. + +## Table of contents + +1. [Status taxonomy](#status-taxonomy) +2. [SDK install + version (`S-INSTALL-*`)](#sdk-install--version-s-install-) +3. [Module activation (`S-MODULE-*`)](#module-activation-s-module-) +4. [Invocation events (`S-INVOKE-*`)](#invocation-events-s-invoke-) +5. [Identity + attributes (`S-IDENTITY-*`)](#identity--attributes-s-identity-) +6. [Feature flags (`S-FLAG-*`)](#feature-flags-s-flag-) +7. [Custom logging (`S-LOG-*`)](#custom-logging-s-log-) +8. [Masking / privacy config (`S-MASK-*`)](#masking--privacy-config-s-mask-) +9. [dSYM / mapping upload (`S-SYMBOL-*`)](#dsym--mapping-upload-s-symbol-) +10. [Build system (`S-BUILD-*`)](#build-system-s-build-) +11. [Privacy view modifiers (`S-PRIVACY-*`)](#privacy-view-modifiers-s-privacy-) +12. [Platform applicability matrix](#platform-applicability-matrix) + +## Status taxonomy + +Same eight statuses as runtime audit (see `check-catalog.md`). Static-specific notes: + +- `PASS` — pattern found and matches expectations (where applicable) +- `FAIL` — required pattern missing, or anti-pattern present +- `WARN` — pattern present but suboptimal (e.g. default value, deprecated API) +- `INFO` — informational signal; not an assertion +- `SKIP` — file class absent from the project so the check can't run (e.g. no `.swift` files in an Android-only repo) +- `DISABLED` — rule pack explicitly turns the check off +- `N/A` — check doesn't apply on this platform (see applicability matrix below) +- `MANUAL` — finding requires human verification (e.g. "tokens detected; verify scope") + +Findings cite **file path + 1-indexed line range** as evidence. Matched text is omitted unless the matched substring is a known-safe identifier (an API name, a module name, etc.). Tokens, secrets, URLs, and contiguous source regions are never quoted. + +## SDK install + version (`S-INSTALL-*`) + +| Code | Check | Evidence | +| --- | --- | --- | +| `S-INSTALL-001` | SDK declared in the platform's package manifest | iOS: `Podfile` / `Package.resolved` / `Cartfile.resolved` line; Android: `build.gradle`(`.kts`) line; Flutter: `pubspec.yaml` line; RN: `package.json` line | +| `S-INSTALL-002` | Installed version matches `expected_sdk_version` from rule pack (when set) | Manifest line + parsed version | +| `S-INSTALL-003` | SDK init call site found (`Luciq.start*`, `Luciq.Builder().build()`, etc.) | File path + line of init call | +| `S-INSTALL-004` | No legacy Instabug references coexist with Luciq (relevant during migration) | Files containing `import Instabug` or equivalent | + +## Module activation (`S-MODULE-*`) + +Per-module toggles. Most modules default ON when the SDK is installed; defaults are platform-specific (see each row's `Default` column below and verify against the live integration guide if uncertain — defaults can change between SDK versions). + +When no toggle pattern is found in source for a default-ON module, the audit emits `INFO` ("no explicit toggle in source; assumed default-ON — runtime audit confirms"), not `PASS` — static analysis alone cannot confirm runtime behaviour. A toggle pattern set to `false` emits `DISABLED` with the file:line citation. The runtime audit then cross-checks: an `S-MODULE- DISABLED` finding causes every dependent `C*` rule to `SKIP` with reason `"module disabled in source (S-MODULE- at :)"` — see `SKILL.md` Phase 5 for the coordination mechanism. + +| Code | Module | Default | Toggle pattern | +| --- | --- | --- | --- | +| `S-MODULE-BR` | Bug Reporting | ON | `BugReporting.enabled`, `Luciq.setBugReportingEnabled`, `BugReporting.setState` | +| `S-MODULE-CRASH` | Crash Reporting | ON | `CrashReporting.enabled`, `Luciq.setCrashReportingEnabled` | +| `S-MODULE-APM` | APM | ON | `APM.enabled`, `Luciq.setAPMEnabled` | +| `S-MODULE-SR` | Session Replay | ON | `SessionReplay.setState`, `Luciq.setSessionReplayEnabled` | +| `S-MODULE-NLG` | Network Logs | ON | `NetworkLogger.enabled`, `Luciq.setNetworkLogging` | +| `S-MODULE-USTEPS` | User Steps | ON | `Luciq.trackUserSteps` | +| `S-MODULE-ANR` | ANR Monitor | ON | `CrashReporting.appHangEnabled`, `Luciq.setANRMonitorEnabled` | +| `S-MODULE-OOM` | OOM Monitor | ON | iOS only — `CrashReporting.setOOMReportingEnabled` | +| `S-MODULE-NDK` | NDK | OFF | Android only — `LuciqNDK.init()` or NDK gradle dependency | +| `S-MODULE-SURVEYS` | Surveys | ON | `Surveys.enabled`, `Luciq.setSurveysEnabled` | +| `S-MODULE-REPLIES` | Replies | ON | `Replies.enabled`, `Luciq.setRepliesEnabled` | +| `S-MODULE-FR` | Feature Requests | ON | `FeatureRequests.enabled` | +| `S-MODULE-FRESTART` | Force Restart | ON | iOS only — `CrashReporting.forceRestartEnabled` | +| `S-MODULE-NETMASK` | Network Auto-Masking | ON | `Luciq.setNetworkAutoMaskingState` | + +## Invocation events (`S-INVOKE-*`) + +| Code | Check | Evidence | +| --- | --- | --- | +| `S-INVOKE-001` | At least one invocation event configured | `Luciq.start(... invocationEvents:)`, `LuciqInvocationEvent.*` | +| `S-INVOKE-002` | No conflicting invocations (e.g. both `none` AND a real event in different code paths) | Multiple init call sites with mismatched events | +| `S-INVOKE-PROG` | Programmatic invocation present | `Luciq.show(`, `Luciq.invoke(`, `BugReporting.show(`, `BugReporting.invoke(` | +| `S-INVOKE-NONE` | Invocation explicitly `none` | `.none` in invocation event setter | + +Supported event values: `shake`, `screenshot`, `floatingButton`, `twoFingersSwipeLeft`, `twoFingersSwipe`, `rightEdgePan`, `none`. + +## Identity + attributes (`S-IDENTITY-*`) + +| Code | Check | Evidence | +| --- | --- | --- | +| `S-IDENTITY-USER` | User identification call site present | `Luciq.identifyUser`, `Luciq.setUserData` | +| `S-IDENTITY-LOGOUT` | User logout hook present | `Luciq.logOutUser` | +| `S-IDENTITY-ATTR` | User attribute APIs in use | `addUserAttribute`, `setUserAttribute`, `userData` property | +| `S-IDENTITY-CDATA` | Custom data APIs in use | `setCustomData`, `Luciq.userData` | + +## Feature flags (`S-FLAG-*`) + +| Code | Check | Evidence | +| --- | --- | --- | +| `S-FLAG-ADD` | `addFeatureFlag(s)` call sites | matched pattern in source | +| `S-FLAG-REMOVE` | `removeFeatureFlag(s)` call sites | matched pattern | +| `S-FLAG-CLEAR` | `removeAllFeatureFlags` / `clearAllFeatureFlags` call sites | matched pattern | +| `S-FLAG-CHECK` | `checkFeatures` call sites | matched pattern | + +## Custom logging (`S-LOG-*`) + +| Code | Check | Evidence | +| --- | --- | --- | +| `S-LOG-API` | Custom log API in use | `Luciq.log*`, `LCQLog.log*`, ObjC macros `LCQLogInfo`, etc. | +| `S-LOG-USEREVENT` | User-event logging in use | `Luciq.logUserEvent(` | + +## Masking / privacy config (`S-MASK-*`) + +| Code | Check | Evidence | +| --- | --- | --- | +| `S-MASK-NETWORK` | Network auto-masking state | `setNetworkAutoMaskingState`, observed enum value | +| `S-MASK-SCREEN` | Screenshot/replay masking mode | `MaskingType.MEDIA`, `MaskingType.LABELS`, `setReplaceCapturedSensitiveData` | +| `S-MASK-HEADERS` | Sensitive headers list configured | Configuration of `Authorization`, `Cookie`, `X-API-Key`, `Set-Cookie` redaction in source | +| `S-MASK-CALLBACK` | Custom request/response masking callback present | `setNetworkLogRequestCompletionHandler` or equivalent | + +## dSYM / mapping upload (`S-SYMBOL-*`) + +iOS uses dSYMs; Android uses ProGuard/R8 mapping files. Both required for production crash symbolication. + +| Code | Platform | Check | Evidence | +| --- | --- | --- | --- | +| `S-SYMBOL-IOS-UPLOAD` | iOS | dSYM upload shell script present | Detected file: `luciq_dsym_upload.sh` / `upload_symbols.sh` / `upload_dsym.sh` (or legacy `instabug.sh`) | +| `S-SYMBOL-IOS-PHASE` | iOS | Run-script build phase invokes the upload script | `project.pbxproj` shell-script phase referencing the upload script | +| `S-SYMBOL-IOS-DWARF` | iOS | Debug Information Format includes DWARF with dSYM | `project.pbxproj` `DEBUG_INFORMATION_FORMAT = dwarf-with-dsym` for Release | +| `S-SYMBOL-AND-PLUGIN` | Android | Luciq mapping upload Gradle plugin applied | `apply plugin: 'luciq.upload'` or KTS equivalent in `build.gradle` | +| `S-SYMBOL-AND-TOKEN` | Android | Mapping upload token configured | `luciqUpload { applicationToken = "…" }` block | + +## Build system (`S-BUILD-*`) + +| Code | Platform | Check | Evidence | +| --- | --- | --- | --- | +| `S-BUILD-IOS-SPM` | iOS | Swift Package Manager in use | `Package.resolved` present | +| `S-BUILD-IOS-PODS` | iOS | CocoaPods in use | `Podfile` (+ `Podfile.lock`) present | +| `S-BUILD-IOS-CART` | iOS | Carthage in use | `Cartfile` (+ `Cartfile.resolved`) present | +| `S-BUILD-AND-GROOVY` | Android | Gradle Groovy in use | `build.gradle` (non-`.kts`) present | +| `S-BUILD-AND-KTS` | Android | Gradle KTS in use | `build.gradle.kts` present | +| `S-BUILD-RN-NPM` | RN | npm-style lockfile present | `package-lock.json` | +| `S-BUILD-RN-YARN` | RN | Yarn lockfile present | `yarn.lock` | +| `S-BUILD-RN-PNPM` | RN | pnpm lockfile present | `pnpm-lock.yaml` | +| `S-BUILD-FLUTTER` | Flutter | Flutter pub | `pubspec.yaml` present | + +Multiple results are valid (e.g. iOS project can ship SPM + Pods). FAIL only when the SDK is not findable in any detected manifest. + +## Privacy view modifiers (`S-PRIVACY-*`) + +iOS-only. Marks SwiftUI / UIKit views that should not be captured by Session Replay. + +| Code | Check | Evidence | +| --- | --- | --- | +| `S-PRIVACY-SWIFTUI` | `.luciqPrivate()` view modifier usage | matched in `*.swift` | +| `S-PRIVACY-UIKIT` | UIKit equivalent privacy marker present | matched in `*.swift` / `*.m` | + +## Platform applicability matrix + +| Code family | iOS | Android | Flutter | React Native | +| --- | --- | --- | --- | --- | +| `S-INSTALL-*` | ✓ | ✓ | ✓ | ✓ | +| `S-MODULE-*` | most ✓ (no NDK) | most ✓ (no OOM, no FRESTART) | subset ✓ | subset ✓ | +| `S-INVOKE-*` | ✓ | ✓ | ✓ | ✓ | +| `S-IDENTITY-*` | ✓ | ✓ | ✓ | ✓ | +| `S-FLAG-*` | ✓ | ✓ | ✓ | ✓ | +| `S-LOG-*` | ✓ | ✓ | ✓ | ✓ | +| `S-MASK-NETWORK` | ✓ | ✓ | ✓ | ✓ | +| `S-MASK-SCREEN` | ✓ | ✓ | ✓ | ✓ | +| `S-SYMBOL-*` | iOS-specific ✓ | Android-specific ✓ | N/A | N/A | +| `S-BUILD-*` | iOS rows ✓ | Android rows ✓ | Flutter row ✓ | RN rows ✓ | +| `S-PRIVACY-*` | ✓ | N/A | N/A | N/A | + +When a check's platform applicability is `N/A`, the audit emits the code with status `N/A` rather than omitting it — keeps the report shape stable across platforms.