Skip to content

GES — Gap & edge-case review

Severity: Critical / High / Medium / Low. Disposition: Add (fold into spec), Defer (post-1.0), Non-goal (document as out of scope), Fixed (already corrected).

  • R1 [Fixed] §9.1.2 still referenced the pre-0.3 trailers GES-Base-Position + GES-Count; corrected to GES-Position in v0.11.0. Action for Claude Code: grep the spec for other stale references to the batch/GES-Count model that the one-event-per-commit switch (§5.2) obsoleted, and for refs/ges/shadow/snapshots/streams name consistency.
  • R2 [H · Add] Event schema evolution / upcasting. The spec versions the record format (gesVersion) but not the payload schema of an event type. Over years, OrderPlaced v1 → v2 will happen; readers must upcast old events. There is no upcasting hook, no per-type schema version, no migration story. This is a core event-sourcing concern and its absence will bite. Recommend a per-event optional schemaVersion and an upcaster stage in readStream/aggregateStream.
  • R3 [C · Add] Interop test vectors. For a standard, two implementations must agree byte-for-byte on JCS, event id, and the integrity chain (predecessorHash, §8.3). Without published test vectors (canonical input → expected JCS bytes → expected sha256: id → expected integrity-chain hash), independent implementations will silently diverge and fail to interoperate — defeating the point of a shared spec. Recommend a normative test-vectors/ fixture set.
  • R4 [H · Add] $all / cross-stream subscription & projection idempotency. §16c covers per-stream cursors, but many read models need “all events across all streams” (Emmett/ESDB $all). The mandatory global stream (§20, C22) now provides this enumeration and its total order. Still unaddressed: catch-up idempotency — if the cursor advances but the read-model write fails and replays, events double-apply unless projections are idempotent or cursor+write is transactional. Recommend: define a documented $all enumeration, require idempotent projections, and specify cursor-advance-after-apply ordering.
  • R5 [H · Add] Snapshot invalidation on evolve change. A snapshot (§13) is a fold under a specific evolve implementation. If evolve changes (bug fix, new field), existing snapshots are stale and silently wrong. Snapshots are not versioned by evolve/logic identity. Recommend a logicVersion/evolveHash field on snapshots and a rule to ignore snapshots whose logic version differs.
  • R6 [M · Partly fixed] Empty/degenerate inputs. Zero-event append and non-existent stream reads are now specified (§10.5.2). Still open: maximum ref-component length vs. very long stream names (encode/hash fallback), very large single event blobs (perf/backpressure), and a stream consisting only of a snapshot with all events truncated.
  • R7 [M · Add] Crash recovery & corruption. §11 explains atomicity (orphaned objects on crash) but there is no operational procedure: when to git fsck, how to reclaim orphaned append objects, and what happens if a projection cursor or shadow ref points at a GC-pruned commit. Recommend a verify/recover command spec and a rule that dangling cursors reset to the nearest reachable position.
  • R8 [M · Add] Clock skew / timestamp trust. Records carry wall-clock timestamp (§6.2). Across clones/worktrees clocks skew; any projection ordering by timestamp across streams is unsound. The spec never states that timestamp is descriptive, not an ordering key. Recommend an explicit statement: order by position/ancestry, never by timestamp.
  • R9 [M · Add] Crypto-shredding key management. §13.3 names crypto-shredding but not where keys live, how they are derived per subject, rotated, or destroyed — the hard part. Recommend a key-management appendix or an explicit “out of scope, implementer must define” note.
  • R10 [M · Non-goal? · Add note] Confidentiality granularity. Repo read access = read every event on every readable ref; there is no per-stream/per-tenant/field-level access control. Multi-tenant deployments need this. Either document as a non-goal or specify repo-per-tenant topology.
  • R11 [L · Add] Trailer spoofing. Denormalized trailers (GES-Tag, GES-Position) are derived from event content; event data containing trailer-like text must never be interpreted as authoritative, and denormalization must sanitize. The spec says the record is authoritative (good) but should state the sanitize/never-parse-trailers-as-truth rule explicitly.
  • R12 [M · Add] Signature key lifecycle. §7.4/§15 recommend signing but omit rotation, revocation, and compromise handling (a compromised key means re-anchoring trust). Recommend a short key-lifecycle subsection.
  • R13 [M · Add] Bootstrapping & migration. No ges init (namespace setup, config core.fsync/fsyncMethod, ref backend choice) and no migration path from an existing event store beyond JSONL import. Recommend an init/migration section.
  • R14 [L · Defer] Observability. No metrics for projection lag, loose-object count, pack ratio, store growth. Useful operationally; defer to implementation.
  • R15 [M · Add] Emmett wiring. Emmett’s event kind: 'Event' is absent from the §6.2 record; and the decider/CommandHandler integration (how GitEventStore plugs into Emmett command handling) is assumed but never shown. Recommend adding kind to the record and a short binding note.
  • R16 [M · Add] Conformance profiles. §16 mixes always-required MUSTs with “(if offered)” items but defines no named profiles (e.g. Core, Distributed, Agent-capture, DCB). Claude Code and adopters need to know the minimal conforming subset. Recommend explicit profiles.
  • R17 [M · Add] Performance/capacity acceptance criteria. Despite extensive performance analysis, §16.1 has only functional tests — no throughput/latency/size thresholds. Recommend at least a capacity-measurement harness (append latency at L2, packed bytes/event, export throughput) even if thresholds stay deployment-defined.
  • R18 [L · Defer] Spec-internal ADRs. The deliberately-abandoned axes (N1 sync HA, the substrate’s per-stream-only order before sequencing, precise cross-stream consistency without authority) are stated in-line but not collected as referenceable decisions. Recommend an ADR appendix (previously offered).
  • R19 [M · Open decision] CloudEvents-compatible record. GES defines its own record schema (§6.2); the mature stores standardize on CloudEvents (EventSourcingDB explicitly). Adopting a CloudEvents-compatible envelope (specversion/id/subject/type/data, plus traceparent/tracestate) would buy interoperability and familiarity at the cost of extra fields. Captured in the §21 register and §19 open questions; resolve against a running implementation, not on paper.
  • R20 [L · Open decision] Ad-hoc query layer. GES serves ad-hoc queries only by native scan (§16b); EventSourcingDB offers EventQL (cross-subject queries, NDJSON, for DCB). Whether GES should define a richer query layer beyond scan + projections is open (§21, §19).
  • R21 [process] Alignment register. §21 now maps GES to the cross-store consensus (EventStoreDB, EventSourcingDB, Marten, Message DB, Emmett) and records each deviation with justification. This is the standing mechanism for the “follow best practice, deviate only with reason” principle; it MUST be updated whenever a decision changes its relationship to the consensus.
  • R22 [process] Ubiquitous-language table. §2.1 now disambiguates each borrowed term (commit, write, append, stream, stream/global position, version, chain, index, snapshot, log, canonical, aggregate, tag, leader/sequencer/authority, origin, writer/appender, decider, ref, projection, merge, id, shared) by linguistic context (bounded context) and pins GES’s meaning. This resolves the recurring cross-context ambiguity (notably “commit”: Git object vs. PostgreSQL transaction point vs. GES event) and, per DDD, a change to a term there is a change to the model — so §2.1 MUST be kept current with any terminology change, in both documents.
  • R23 [process] Core / framework-binding separation. GES is standalone infrastructure (§1): a framework-neutral, stream-based core (§10.0) with Emmett as one reference binding (§10.1), not the definition. Conformance (§16.1) is framework-independent; the Emmett binding is an optional profile. DCB (dcb.events) is an optional additional interface (§18c), a different (stream-less) model — not the core vocabulary, and not reachable through Emmett (Emmett has no DCB). DCB terminology applies only inside that layer. The tag notion does not carry across models (DCB tag ≠ Git tag ≠ Emmett data/metadata field) — the earlier over-unification of these is the specific error this note guards against.

Highest-leverage before implementation: R3 (interop vectors) and R2/R5 (schema & snapshot versioning) are the ones whose absence causes silent, hard-to-debug divergence; R4 (projection idempotency) is the one most likely to cause a real bug in the first read model. R19 (CloudEvents) is the highest-value alignment decision — worth prototyping both envelopes early.

Git Event Store is an independent open specification for an event store that uses Git as its storage component. It is not affiliated with or endorsed by the Git Project or Software Freedom Conservancy. Git and the Git logo are either registered trademarks or trademarks of Software Freedom Conservancy, Inc., corporate home of the Git Project, in the United States and/or other countries.

Git Event Store is not affiliated with or endorsed by Kurrent, the maker of KurrentDB (formerly EventStoreDB). "Event store" is used here as the generic name of the event-sourcing storage pattern.