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 authoritative behavioral contract of Quartz's event stores — `IEventStore` and its reference SQLite implementation (`nip01Core/store/sqlite/`). Use when implementing or asserting parity with a Quartz event store (external engines like Vespa, the filesystem store, geode),
$ npx -y skills add vitorpamplona/amethyst --skill event-store-semantics --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/event-store-semanticsContext preview
The summary Claude sees to decide when to auto-load this skill.
The authoritative behavioral contract of Quartz's event stores — `IEventStore` and its reference SQLite implementation (`nip01Core/store/sqlite/`). Use when implementing or asserting parity with a Quartz event store (external engines like Vespa, the filesystem store, geode),
name: event-store-semantics description: The authoritative behavioral contract of Quartz's event stores — `IEventStore` and its reference SQLite implementation (`nip01Core/store/sqlite/`). Use when implementing or asserting parity with a Quartz event store (external engines like Vespa, the filesystem store, geode), answering filter-semantics questions (since/until inclusivity, tag OR/AND, multi-filter limits, ordering tiebreaks), or working on the write-path rules for replaceable/addressable supersession, NIP-09 deletions, NIP-40 expiration, NIP-62 vanish, NIP-45 counts, or NIP-50 search inside the store. Every behavior has a named rule id (STORE-Fxx/Wxx/Dxx/Sxx/Cxx) so downstream implementations can annotate divergences precisely.
The SQLite `EventStore` (`quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/`) is the de-facto **reference implementation** of what a Quartz event store must do. Other implementations — the in-repo filesystem store (`nip01Core/store/fs/`, held to parity by `quartz/src/jvmTest/.../store/fs/FsParityTest.kt`) and external engines (e.g. a Vespa-backed store) — reimplement its *observable behavior* and assert parity in CI. This skill states that behavior as **named, numbered decisions** so a parity divergence becomes a lookup, not an archaeology session through `QueryBuilder`/`MergeQueryExecutor`.
Every rule below was verified against the code as of this skill's last update. When you change store behavior, **update the rule here in the same PR** and add a line to the [Semantics changelog](#semantics-changelog) — downstream implementations pin Quartz by commit and review pin bumps against this file.
| Concern | File | |---|---| | Public contract (KDoc is normative) | `nip01Core/store/IEventStore.kt` | | High-level store (owns pool + planner) | `sqlite/EventStore.kt`, `sqlite/SQLiteEventStore.kt` | | Filter → SQL, ordering, limits, counts | `sqlite/QueryBuilder.kt` | | k-way merge fast path (feed shapes) | `sqlite/MergeQueryExecutor.kt` | | Schema, tag hashing, immutability | `sqlite/EventIndexesModule.kt`, `sqlite/TagNameValueHasher.kt`, `sqlite/SeedModule.kt` | | Replaceable / addressable supersession | `sqlite/ReplaceableModule.kt`, `sqlite/AddressableModule.kt` | | NIP-09 / NIP-40 / NIP-62 / ephemeral | `sqlite/DeletionRequestModule.kt`, `sqlite/ExpirationModule.kt`, `sqlite/RightToVanishModule.kt`, `sqlite/EphemeralModule.kt` | | NIP-50 FTS | `sqlite/FullTextSearchModule.kt` (see also the `searchable-events` skill) | | Index/feature toggles | `sqlite/IndexingStrategy.kt` (client default) and geode's `RelayIndexingStrategy.kt` (relay preset) | | Operational README | `sqlite/README.md` (concurrency, pragmas, maintenance) |
Executable spec: the test suites in `quartz/src/commonTest/.../store/sqlite/` (`BasicTest`, `ReplaceableTest`, `AddressableTest`, `DeletionTest`, `ExpirationTest`, `RightToVanishTest`, `SearchTest`, `SearchRelevanceOrderTest`, `MergeQueryCorrectnessTest`, `TagMergeCorrectnessTest`, `QueryAssemblerTest`, `SnapshotIdsForNegentropyTest`, `FilterMatcherTest`, …). If a rule here ever contradicts a test, the test wins — and this file has a bug to fix.
---
**STORE-F01 — `since`/`until` are both inclusive.** `since` compiles to `created_at >= ?`, `until` to `created_at <= ?` (`QueryBuilder` uses `greaterThanOrEquals`/`lessThanOrEquals` everywhere). An event with `created_at == since == until` matches.
**STORE-F02 — `ids` and `authors` are exact-match only.** They compile to `=`/`IN` against the full 64-char hex columns. **NIP-01 prefix matching is NOT supported** anywhere in the store. (`Filter`'s constructor logs an error for non-64-char ids/authors but still sends them; they simply never match.)
**STORE-F03 — tag filter combination.** Within one tag name, values are **OR** (`tag_hash IN (…)`). Across different tag names in the same filter, conditions are **AND** (each extra name becomes another `event_tags` self-join). `tagsAll` (NIP-91 `&x` syntax) demands **every listed value** be present on the event — one join + equality per value — and composes by AND with any plain `tags` in the same filter.
**STORE-F04 — only single-letter tag names are indexed (by default).** `DefaultIndexingStrategy.shouldIndex` indexes a tag iff `tag.size >= 2 && tag[0].length == 1`. A filter on a multi-letter tag name (`#title`, `#alt`) matches **nothing** in the SQLite store. Deployments can widen `shouldIndex`, but the stock contract is single-letter-only.
**STORE-F05 — `d` is special-cased out of the tag index.** `#d` values are matched against the `event_headers.d_tag` column, not `event_tags` (`Filter.toFilterWithDTags()`). Consequences: `#d` works on addressable events (which populate `d_tag`); when all `kinds` are addressable the query adds `kind >= 30000 AND kind < 40000` to pin the addressable index. **Only use `#d` via plain `tags`.** A `#d` under `tagsAll` is handled inconsistently: on the simple (no other tags/search) path it degrades to OR semantics (`toFilterWithDTags` folds it into `dTags`), and when `tags["d"]` is also present it is dropped entirely; on the tag-join path it is ignored. (An event has one d-tag, so AND-across-values could never match anyway.)
**STORE-F06 — tag and author matching in the tag path is hash-based.** `event_tags` stores a 64-bit MurmurHash3 of `(tag name, value)` keyed by a per-database random seed (`SeedModule`, `TagNameValueHasher`); the p/e/a-owner columns are hashes too. There is **no post-verification** of hash matches, so a hash collision would return a false positive. Probability is negligible in practice but nonzero — a parity harness comparing ag
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…