# BUGSWEEPER Flexibility Update - Research

**Researched:** 2026-03-25
**Domain:** Rust HTTP debug server, Slint property/callback access, SQLite query validation
**Confidence:** HIGH

## Summary

BUGSWEEPER's four shortcomings all stem from the same root: both the property registry and callback registry are `match` statements hardcoded in `crates/app/src/main.rs`, and the SQL validator uses a strict allowlist that does not account for read-only PRAGMA statements. Slint's compile-time code generation provides no runtime introspection API — there is no way to enumerate all properties and callbacks from a `DashboardWindow` instance at runtime. Therefore, "dynamic discovery" must mean: (1) a data-driven registry defined at startup rather than a giant `match`, and (2) a `GET /api/ui/registry` endpoint that reflects that registry back to callers. This is achievable without any Slint API changes and without introducing `slint-interpreter`.

The PRAGMA fix is independent and simple: change the SQL validator in `SqliteStore::query_raw` to allow `PRAGMA` as a valid statement start alongside `SELECT`.

**Primary recommendation:** Replace hardcoded `match` blocks with a `HashMap`-based registry populated at startup, add a registry introspection endpoint, and extend the SQL validator to allow `PRAGMA` reads.

---

## Current Architecture Analysis

### Where the hardcoding lives

| File | Location | What is hardcoded |
|------|----------|-------------------|
| `crates/app/src/main.rs` | `get_property()` ~L1160 | `match name` over 15 readable property names |
| `crates/app/src/main.rs` | `set_property()` ~L1197 | `match name` over 8 writable property names with type dispatch |
| `crates/app/src/main.rs` | `invoke_callback()` ~L1254 | `match name` over ~30 callback names with arg parsing |
| `crates/service/src/db/sqlite.rs` | `query_raw()` L482–492 | Only `SELECT` allowed as prefix; PRAGMA rejected |

### Phase 17 additions that are missing

**Missing properties (readable + writable):**
- `add-product-visible` — `bool`, r/w (`get_add_product_visible` / `set_add_product_visible`)
- `add-product-name` — `string`, r/w
- `add-product-shopify-url` — `string`, r/w
- `add-product-validation-error` — `string`, r/w
- `product-detail-visible` — `bool`, r/w

**Missing callbacks (no args):**
- `on-product-add-clicked` — maps to `invoke_on_product_add_clicked()`
- `on-add-product-submit` — maps to `invoke_on_add_product_submit()`
- `on-add-product-discard` — maps to `invoke_on_add_product_discard()`
- `on-product-detail-close` — maps to `invoke_on_product_detail_close()`

**Missing callbacks (string arg):**
- `on-product-set-image(string)` — maps to `invoke_on_product_set_image(product_id)`
- `on-product-view-shopify(string)` — maps to `invoke_on_product_view_shopify(product_id)`
- `on-product-ellipsis-detail(string)` — maps to `invoke_on_product_ellipsis_detail(product_id)`
- `on-product-archive(string)` — maps to `invoke_on_product_archive(product_id)` (no-op currently)
- `on-product-create-unit(string)` — maps to `invoke_on_product_create_unit(product_id)`
- `on-add-product-name-changed(string)` — maps to `invoke_on_add_product_name_changed(val)`
- `on-add-product-shopify-url-changed(string)` — no-op
- `on-add-product-suggestion-selected(string)` — no-op
- `on-product-unit-clicked(string)` — no-op

---

## Slint Introspection — What is NOT Available

**Finding (HIGH confidence):** The project uses compile-time Slint bindings (`slint = "1"`, `slint-build = "1"`) — the `.slint` files are compiled into a generated Rust module. The resulting `DashboardWindow` struct has typed getter/setter methods but **no reflection or iteration API**. There is no `list_properties()`, `list_callbacks()`, or equivalent on a compiled Slint component.

The `slint-interpreter` crate provides `ComponentDefinition::properties()` and `ComponentDefinition::callbacks()`, but this crate is NOT used by the project and cannot be retrofitted — it requires compiling `.slint` files at runtime through a separate interpreter pipeline. Adding it just for introspection would be a major dependency increase with no other benefit.

**Conclusion:** True dynamic introspection is not feasible without a significant architectural change. The correct approach is a **data-driven static registry** — a `HashMap` populated at startup that enumerates all known properties/callbacks. This moves the inventory from implicit `match` arms to an explicit, maintainable data structure.

---

## Architecture Patterns

### Pattern 1: Data-Driven Registry in `AppBugsweeperBackend`

Replace the three `match` blocks with three `HashMap`s built at construction time. Each map holds a closure (or a type tag + closure).

**Readable properties** — `HashMap<String, Box<dyn Fn(&DashboardWindow) -> serde_json::Value + Send + Sync>>`

Problem: `DashboardWindow` is not `Send`. The closures must be called inside `query_ui`, which already handles the thread hop. The registry just needs to hold the name-to-read-logic mapping; execution stays inside `query_ui`.

Simplest viable approach: store property names + type metadata in a `Vec<PropertyEntry>` where each entry has `name: &'static str`, `type_tag: PropertyType`, `readable: bool`, `writable: bool`. The match dispatch inside `query_ui` can then iterate the entries for the "unknown" error message, while the actual dispatch can remain a `match` that is now data-verified at compile time.

The alternative (true closure-based dispatch) requires `Box<dyn Fn(...)>` held in a `HashMap`. Since closures capturing `Weak<DashboardWindow>` are `Send` but `DashboardWindow` itself is only accessed inside `invoke_from_event_loop`, this is feasible — but adds complexity. Given that the Slint bridge already requires going through `query_ui` for every UI read, the simplest winning pattern is:

**Recommended: metadata table + match dispatch**

```rust
// In bugsweeper_impl or a new bugsweeper_registry.rs
pub struct PropEntry {
    pub name: &'static str,
    pub type_tag: &'static str,  // "bool", "string", "int", "float"
    pub readable: bool,
    pub writable: bool,
}

pub const PROPERTY_REGISTRY: &[PropEntry] = &[
    PropEntry { name: "search-text",          type_tag: "string", readable: true,  writable: true  },
    PropEntry { name: "active-mode-index",    type_tag: "int",    readable: true,  writable: true  },
    PropEntry { name: "add-product-visible",  type_tag: "bool",   readable: true,  writable: true  },
    // ... etc
];
```

The `match` in `get_property` / `set_property` remains (Slint's typed API requires it), but now:
- The registry is the single place to add new properties
- The error message is auto-generated from the registry
- The `GET /api/ui/registry` endpoint can serialize PROPERTY_REGISTRY directly

**Callback registry** — same pattern, `CallbackEntry` with `name`, `arg_types: &[&str]`, `description`:

```rust
pub struct CallbackEntry {
    pub name: &'static str,
    pub args: &'static [&'static str],  // e.g. &["int", "string"]
}

pub const CALLBACK_REGISTRY: &[CallbackEntry] = &[
    CallbackEntry { name: "refresh-all-clicked",     args: &[] },
    CallbackEntry { name: "on-product-add-clicked",  args: &[] },
    CallbackEntry { name: "on-add-product-submit",   args: &[] },
    // ...
];
```

### Pattern 2: New `GET /api/ui/registry` Endpoint

Add a new endpoint that returns the full registry for agent-side discovery:

```
GET /api/ui/registry
```

Response:
```json
{
  "properties": [
    {"name": "search-text", "type": "string", "readable": true, "writable": true},
    {"name": "add-product-visible", "type": "bool", "readable": true, "writable": true}
  ],
  "callbacks": [
    {"name": "refresh-all-clicked", "args": []},
    {"name": "on-add-product-submit", "args": []}
  ]
}
```

This endpoint requires:
1. A new `fn get_registry(&self) -> serde_json::Value` method on `BugsweeperBackend` trait (in `router.rs`)
2. Implementation in `AppBugsweeperBackend` that serializes the static registry tables
3. Route added to `route_request` in `router.rs`
4. Entry added to `REGISTERED_ENDPOINTS`

### Pattern 3: PRAGMA Allowlist in `query_raw`

Current validation in `SqliteStore::query_raw` (`crates/service/src/db/sqlite.rs` L482–492):

```rust
if !upper.starts_with("SELECT") {
    return Err("Only SELECT queries allowed".into());
}
```

Fix: accept PRAGMA statements that do not mutate. The safe PRAGMA reads are all the informational ones (`table_info`, `index_list`, `foreign_key_list`, `integrity_check`, `journal_mode`, etc.). The mutating ones are things like `PRAGMA journal_mode = WAL` (with assignment).

**Simplest safe fix:** Allow `PRAGMA` prefix, but reject if the uppercased statement contains `=` (assignment syntax). Pure reads (`PRAGMA table_info(x)`, `PRAGMA integrity_check`) do not use `=`.

```rust
let is_select = upper.starts_with("SELECT");
let is_safe_pragma = upper.starts_with("PRAGMA") && !upper.contains('=');

if !is_select && !is_safe_pragma {
    return Err("Only SELECT or read-only PRAGMA queries allowed".into());
}
```

This allows:
- `PRAGMA table_info(recipients)` — schema inspection
- `PRAGMA integrity_check` — data validation
- `PRAGMA index_list(cards)` — index inspection
- `PRAGMA foreign_key_list(cards)` — relationship inspection

This blocks:
- `PRAGMA journal_mode = WAL` — mode change
- `PRAGMA foreign_keys = ON` — setting change

Confidence: HIGH — this is a well-understood SQLite pattern. All informational PRAGMAs use function-call or bare syntax, not `=` assignment.

---

## Implementation Scope

The full change touches three files:

| File | Changes |
|------|---------|
| `crates/app/src/main.rs` | Add `PROPERTY_REGISTRY` + `CALLBACK_REGISTRY` const tables; add Phase 17 entries; add Phase 17 arms to `get_property`, `set_property`, `invoke_callback`; implement `get_registry()` |
| `crates/bugsweeper/src/router.rs` | Add `get_registry` to `BugsweeperBackend` trait; add `GET /api/ui/registry` route; add to `REGISTERED_ENDPOINTS` |
| `crates/service/src/db/sqlite.rs` | Extend `query_raw` to allow read-only PRAGMA |
| `crates/bugsweeper/GUIDE.md` | Document new endpoint, updated property/callback lists, PRAGMA support |

---

## Don't Hand-Roll

| Problem | Don't Build | Use Instead |
|---------|-------------|-------------|
| URL-decoded SQL parameter | Custom parser | Existing `percent_decode()` already in `router.rs` |
| JSON serialization of registry | Custom writer | `serde_json::json!()` macro |
| Thread-safe Slint property access | New bridge | Existing `bugsweeper::query_ui()` |

---

## Common Pitfalls

### Pitfall 1: Forgetting the `pick-recipient-clicked` callback signature mismatch

`on_pick_recipient_clicked` takes `(int)` in the Slint file but the current BUGSWEEPER callback list shows it as `pick-recipient-clicked(int)`. The new registry must record arg types accurately — agents use this to build correct JSON bodies.

### Pitfall 2: PRAGMA with `=` on different whitespace patterns

The `=` check for PRAGMA safety must handle whitespace: `PRAGMA journal_mode=WAL` (no spaces). The `upper.contains('=')` check handles all forms since `=` cannot appear in a read-only PRAGMA.

### Pitfall 3: `sync-cards-updated` is invoke-only

`sync_cards_updated` is called by the backend to signal the UI, not invoked externally. Including it in the BUGSWEEPER callback registry is OK for testing (it triggers the sync-complete animation), but it should be labeled carefully in the registry description.

### Pitfall 4: `search-result-count` and `refresh-all-disabled` are read-only computed properties

These are set by app logic, not writeable via BUGSWEEPER. They must appear in the readable list but must NOT be added to the writable list.

### Pitfall 5: `on_on_*` naming in generated Slint Rust

Phase 17 callbacks defined as `on-product-add-clicked` in `.slint` generate as `on_on_product_add_clicked()` (double `on_`) in the Rust API. This is already present in the codebase (L2981, L2992, etc.). The BUGSWEEPER-facing name should use the user-friendly form `on-product-add-clicked`, mapped internally to `w.invoke_on_product_add_clicked()`.

---

## Open Questions

1. **Should `GET /api/ui/registry` live in the `bugsweeper` crate or purely in `main.rs`?**
   - What we know: The registry tables will be `const` arrays defined in `main.rs` (since they reference `DashboardWindow`-specific names). The trait method `get_registry` on `BugsweeperBackend` can return a `serde_json::Value` serialized from those tables.
   - What's unclear: Whether it's worth defining a generic `Registry` type in the `bugsweeper` crate.
   - Recommendation: Keep it simple — `get_registry()` returns `serde_json::Value` built from the static tables in `main.rs`. No shared type needed.

2. **Should `query_raw` be changed in the `service` crate (production code path)?**
   - What we know: `query_raw` is also used by BUGSWEEPER only (no other callers in production paths). The PRAGMA extension is purely a debugging convenience.
   - Recommendation: Yes, change it in `service/src/db/sqlite.rs`. The PRAGMA extension is safe (read-only constraint enforced by the `=` check) and the function is debug-only in practice. Document the relaxation in the function's doc comment.

---

## Environment Availability

Step 2.6: SKIPPED — this is a pure code/config change. No external dependencies required beyond existing Rust toolchain and Cargo.

---

## Sources

### Primary (HIGH confidence)
- Direct code reading: `crates/bugsweeper/src/router.rs` — full router and `BugsweeperBackend` trait
- Direct code reading: `crates/app/src/main.rs` L1027–1540 — full `AppBugsweeperBackend` impl
- Direct code reading: `crates/service/src/db/sqlite.rs` L477–520 — `query_raw` implementation
- [slint_interpreter::ComponentDefinition docs](https://docs.rs/slint-interpreter/latest/slint_interpreter/struct.ComponentDefinition.html) — confirmed `properties()` and `callbacks()` exist only on interpreter path

### Secondary (MEDIUM confidence)
- SQLite PRAGMA documentation pattern — PRAGMA assignment syntax uses `=`, read-only syntax uses function-call form; cross-verified against known SQLite usage in the codebase itself (L108–110 show `PRAGMA journal_mode = WAL` with `=`)

---

## Metadata

**Confidence breakdown:**
- Current architecture: HIGH — read directly from source
- Phase 17 missing items: HIGH — identified from grep of main.rs callback wiring
- Slint introspection unavailability: HIGH — confirmed no `slint-interpreter` dependency, compile-time bindings have no reflection
- PRAGMA fix: HIGH — straightforward string check, verified against existing codebase PRAGMA usage
- Registry pattern: HIGH — standard Rust pattern, no external dependencies

**Research date:** 2026-03-25
**Valid until:** 60 days (stable — no external API changes involved)
