Skip to content
Development
Skill

/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),

From plugin
amethyst
1.6k30 skills3 commands
Install
$ npx -y skills add vitorpamplona/amethyst --skill event-store-semantics --agent claude-code

How 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.md
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

Read more
Ships withamethyst

Nostr client for Android

Get the whole plugin

Other skills on amethyst.