/event-store-semantics
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.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/event-store-semantics
Context 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),
SKILL.md
event-store-semantics.SKILL.mdname: 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.
Event Store Semantics — the `IEventStore` / SQLite-store contract
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.
Key files
| 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.
Kind classes (used throughout)
- **Replaceable**: kind `0`, kind `3`, and `10000 ≤ kind < 20000`.
- **Ephemeral**: `20000 ≤ kind < 30000`.
- **Addressable**: `30000 ≤ kind < 40000`.
- Everything else is a regular event.
---
Filter matching (STORE-F)
**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
Read more
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.
Event Store Semantics — the `IEventStore` / SQLite-store contract
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.
Key files
| 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.
Kind classes (used throughout)
- **Replaceable**: kind `0`, kind `3`, and `10000 ≤ kind < 20000`.
- **Ephemeral**: `20000 ≤ kind < 30000`.
- **Addressable**: `30000 ≤ kind < 40000`.
- Everything else is a regular event.
---
Filter matching (STORE-F)
**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
Other skills on amethyst.
- /account-state
Account state and in-memory event store patterns in Amethyst. Use when working with `Account.kt` (per-user state objects — `kind3FollowList`, `nip65RelayList`, `muteList`, `bookmarkState`, each exposing a `.flow` StateFlow), `LocalCache` (the object-level event store backed by
Open skill - /amy-expert
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 new subcommand into `Main.kt`, writing an interop test script that drives Amy, or extracting logic out of `amethyst/`
Open skill - /android-expert
Android platform patterns for the `amethyst/` module. Use when working with (1) Android navigation (Navigation Compose, type-safe routes, bottom nav), (2) runtime permissions (camera, notifications, biometrics), (3) platform APIs (Intent, Context, Activity, ContentResolver), (4)
Open skill - /auth-signers
Signer abstraction patterns in Amethyst. Use when working with event signing, choosing between a local keypair (`NostrSignerInternal`), a remote NIP-46 bunker signer (`NostrSignerRemote`), or a NIP-55 Android external-app signer (`NostrSignerExternal`). Covers the abstract
Open skill - /compose-expert
Advanced Compose Multiplatform UI patterns for shared composables. Use when working with visual UI components, state management patterns (remember, derivedStateOf, produceState), recomposition optimization (@Stable/@Immutable visual usage), Material3 theming, custom ImageVector
Open skill - /compose-modifier-and-layout-style
Use when writing or reviewing Jetpack Compose layout APIs, modifier parameters, modifier chain construction, hardcoded root layout decisions, or layout wrappers around a single conditional. Technique-layer skill — complements the codebase-specific compose-expert.
Open skill

