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
Stats
1,599
Stars
221
Forks
Active
Maintenance
Kotlin
Language
MIT
License
7h ago
Last commit
3y ago
Created

Repo: vitorpamplona/amethyst

Other skills on amethyst.