account-state
Account state and in-memory event store patterns in Amethyst. Use when working with `Account.kt` (per-user state objects — `kind3FollowList`, `nip65RelayList`,…
The NIP-50 indexing surface of Quartz — the `SearchableEvent` interface, which event kinds are searchable, exactly what text each kind's `indexableContent()` contributes, how the SQLite/filesystem stores consume it, and the NIP-50 `SearchQuery` extension grammar plus
$ npx -y skills add vitorpamplona/amethyst --skill searchable-events --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/searchable-eventsContext preview
The summary Claude sees to decide when to auto-load this skill.
The NIP-50 indexing surface of Quartz — the `SearchableEvent` interface, which event kinds are searchable, exactly what text each kind's `indexableContent()` contributes, how the SQLite/filesystem stores consume it, and the NIP-50 `SearchQuery` extension grammar plus
name: searchable-events description: The NIP-50 indexing surface of Quartz — the `SearchableEvent` interface, which event kinds are searchable, exactly what text each kind's `indexableContent()` contributes, how the SQLite/filesystem stores consume it, and the NIP-50 `SearchQuery` extension grammar plus `SearchRelayListEvent` (kind 10007). Use when making a kind searchable, changing what a kind indexes, diffing the searchable set at a Quartz version bump (external search engines mirror this table), debugging why an event is or isn't found by search, or working with search extensions (`include:spam`, `domain:`, …).
`quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip50Search/SearchableEvent.kt`:
interface SearchableEvent {
fun indexableContent(): String
}One method; marker and extractor in one. An event kind is searchable **iff** its event class implements this interface **and** the class is wired into `EventFactory` (the stores probe searchability by kind through `EventFactory.create` — an unwired implementor is invisible).
Rules every implementation follows (keep them when adding one):
metadata-ish kinds use `" "`); no markup stripping is performed — markdown/asciidoc content goes in raw, JSON-content kinds (kind 0 metadata, marketplace stalls, channel info) **parse first and join the extracted fields**, never the raw JSON.
the insert transaction. Parsed-JSON implementations use `?.let { … } ?: ""`.
the public petname/summary/topics, never the NIP-44 payload).
`listOfNotNull(title(), summary(), content)`; lists index `title() + description()`.
**`references/searchable-kinds.md`** in this skill holds the authoritative table — every implementor with its kind number, class, and the exact `indexableContent()` expression (126 concrete classes / 129 kind values as of 2026-08). Diff that file at a version bump to answer "did the searchable set or any kind's indexed text change?".
Notables that surprise people:
(`zapRequest?.content.orEmpty()`) — receipts are searchable by the zapper's comment.
website, picture URL, …).
`SoftwareReleaseEvent`); `EventFactory` resolves 30063 to `ReleaseArtifactSetEvent`, so `title()\ndescription()` is what actually gets indexed — `SoftwareReleaseEvent.indexableContent()` is dead on the store path.
Adding `SearchableEvent` to a kind, removing it, or changing any `indexableContent()` body:
1. **Update `references/searchable-kinds.md`** in the same PR (external search engines — e.g. the Vespa-backed store's `SearchExtractors` — mirror this table at pin bumps; a silent change ships them stale search results). 2. **Remember existing databases don't reindex themselves.** Old rows keep their old (or missing) FTS text until `IEventStore.reindexFullTextSearch()` runs — the KDoc on that method is the contract. App-side, schedule the resumable overload after shipping such a change. 3. New implementors must be **registered in `EventFactory`** or the reindex scan and kind pre-filter (`FullTextSearchModule.isSearchableKind`) will never see them.
Eligibility policy: a kind becomes searchable when it carries human-authored, human-meaningful text (titles, bodies, names, descriptions). Pure-machine kinds (reactions, follow lists, zaps minus their comment, relay lists) stay out to keep the index small.
**SQLite** (`nip01Core/store/sqlite/FullTextSearchModule.kt`): `CREATE VIRTUAL TABLE event_fts USING fts5(content, content='', contentless_delete=1)` — contentless, `rowid` = `event_headers.row_id`, an `AFTER DELETE` trigger keeps it in sync. On insert (when FTS is on and not deferred): `if (event is SearchableEvent)` → bind `event.indexableContent()` — the only method ever called. Tokenization is entirely SQLite's default FTS5 `unicode61`; queries are always a bound `event_fts MATCH ?` (never concatenated), ordered by bm25 `rank` then `created_at DESC`. Query-side semantics (relevance ordering, extension stripping, FTS-off behavior, deferred catch-up) are rules STORE-S01…S06 in the `event-store-semantics` skill.
**Filesystem store** (`jvmMain/.../store/fs/FsIndexer.kt` + `FsSearchTokenizer.kt`): tokenizes `indexableContent()` itself, approximating `unicode61` (split on non-letter/digit, lowercase); the same tokenizer runs on queries so drift cancels.
**`SearchQuery`** (`nip50Search/SearchQuery.kt`) — typed parse of the `search` filter string into `terms` + `extensions`. A whitespace token is an extension iff it looks like `lowercasekey:value` (the value not starting with `//`, so URLs stay free text); duplicate keys keep the last; unknown extensions are preserved (`extension(key)`). Typed accessors: `includeSpam`, `domain`, `language`, `sentiment`, `nsfw`. `stripExtensions()` / `Filter.strippingSearchExtensions()` is the bridge the built-in stores use — unsupported extensions are **ignored** (NIP-50), so an extensions-only search collapses to an unconstrained query, never match-nothing. A server-side store that implements its own extensions (`observer:`, `sort:rank`, …) receives the raw string (see the `IEventStore` KDoc) and should
Account state and in-memory event store patterns in Amethyst. Use when working with `Account.kt` (per-user state objects — `kind3FollowList`, `nip65RelayList`,…
Patterns for extending `amy`, the Amethyst CLI in `cli/`. Use when adding an `amy <verb>` command, touching files under `cli/src/main/kotlin/…/cli/`, wiring a…
Android platform patterns for the `amethyst/` module. Use when working with (1) Android navigation (Navigation Compose, type-safe routes, bottom nav), (2)…
Signer abstraction patterns in Amethyst. Use when working with event signing, choosing between a local keypair (`NostrSignerInternal`), a remote NIP-46 bunker…
Advanced Compose Multiplatform UI patterns for shared composables. Use when working with visual UI components, state management patterns (remember,…
Use when writing or reviewing Jetpack Compose layout APIs, modifier parameters, modifier chain construction, hardcoded root layout decisions, or layout…