This design explains how to split the current shared Rust crate into stable
public crates without exposing service internals as a semver contract.
crates/shared is source-only today. It is consumed by crates/projections and
mixes three different responsibilities:
| Module | Current contents | Public API suitability |
|---|---|---|
types |
EventType, VaultType, IngestionMode, block/log/sequence aliases, ID aliases. |
Mostly public, but enum evolution needs semver annotations. |
models |
StoredEvent, typed MultiVault event payload records, projection checkpoint/read-model rows. |
Split: event envelopes and payloads are public; checkpoint/read-model rows are service/database contracts. |
parsed_event |
ParsedEvent, EventMetadata, parse helpers, ParseError. |
Public if the event record types move with it and the unknown-event fallback remains. |
errors |
IndexerError, retry classification, deprecated helper shims. |
Service-local. It depends on SQLx, Redis, IO, metrics, leader election, and projection concerns. |
config |
Environment readers for database, Redis, blockchain, server config. | Service-local. These are runtime policy, not library primitives. |
locking |
PostgreSQL advisory lock helpers and LockableEvent. |
Private/service-local. It depends on SQLx transactions and database locking policy. |
graph_flags |
Cached env feature flags for SurrealDB/Postgres migration. | Private/service-local. It is deployment transition logic. |
test_utils, proptest_invariants |
Test factories and property suites. | Keep private; move reusable fixtures later only if external tests need them. |
crates/rindexer-ingestion does not currently depend on shared; it owns its
storage/event code. The first migration therefore targets projections, then
optionally converges ingestion after the public contracts are stable.
Stable, dependency-light scalar contracts:
BlockNumber,LogIndex,SequenceNumberTermId,EntityId, transaction/block hash aliases or newtypesEventTypeUnknownEventType
Dependencies should be limited to serde and, only if needed, small no-runtime
helpers. Do not include SQLx, Redis, Tokio, tracing, dotenv, or database pools.
Semver rules:
- Mark externally matched enums
#[non_exhaustive]unless every future variant is intended to be a breaking change. - Keep canonical string parsing strict and case-sensitive.
- Keep
EventType::as_str()as the single source of truth for wire names. - Prefer newtypes before publication if aliases need validation or display guarantees; otherwise document aliases as convenience only.
Event-store envelope and typed MultiVault payload contracts:
StoredEventNewEvent, if external ingestion tools should construct event-store writesEventMetadata,EventMetadataRef- typed payload records:
AtomCreatedRecordTripleCreatedRecordDepositedRecordRedeemedRecordSharePriceChangedRecordProtocolFeeAccruedRecord
ParsedEventParseError
This crate can depend on intuition-core-primitives, chrono, serde,
serde_json, thiserror, and bigdecimal. Keep SQLx derives out of the public
crate unless there is a deliberate sqlx feature, because SQLx ties the crate
to database column contracts and a heavier dependency graph.
Semver rules:
- Mark
ParsedEvent#[non_exhaustive]so new protocol events can be added without forcing downstream exhaustive matches to break unexpectedly. - Mark typed payload structs
#[non_exhaustive]if adding optional decoded fields should be non-breaking. - Preserve
ParsedEvent::Unknown(StoredEvent)as the forward-compatibility escape hatch. - Preserve parse behavior: unknown event type is not an error; known event type
with malformed payload returns
ParseErrororUnknownthroughparse_or_unknown.
Optional later crate for read-model DTOs if external consumers need typed Timescale rows:
VaultPositionSharePriceHistory- stable output DTOs for leaderboard/protocol/term aggregates
Do not publish this in the first split. These types are closer to database and API evolution than protocol event contracts, so they need a separate schema compatibility review.
Keep these source-only inside the services or move them to an internal
crates/service-support crate that is not published:
IndexerErrorand retry classification: service supervision policy differs between ingestion, projections, and future services.DatabaseConfig,RedisConfig,BlockchainConfig,ServerConfig, and env helpers: runtime configuration is deployment policy.- PostgreSQL advisory lock helpers: they depend on SQLx transactions and table locking strategy.
graph_flags: migration/deployment transition logic.- projection checkpoints, ingestion state, reorg rows, and database read-model rows until a public schema compatibility policy exists.
- test-only factories and property suites, except for a future fixtures crate if downstream integrators request it.
This keeps the public Rust API limited to protocol/event contracts that external indexers can reasonably depend on.
- Add
crates/primitiveswith package nameintuition-core-primitives. Move the stable scalar aliases andEventTypethere. - Add
crates/eventswith package nameintuition-core-events. Move event envelopes, typed payload records,ParsedEvent, metadata, and parse errors there. - Turn
crates/sharedinto a compatibility crate inside the workspace: re-export public items from the new crates and keep service-local modules (errors,config,locking,graph_flags) in place. - Update
crates/projectionsimports gradually:shared::types::EventType->intuition_core_primitives::EventTypeshared::models::*RecordandStoredEvent->intuition_core_events::*shared::parsed_event::*->intuition_core_events::*
- Keep
crates/rindexer-ingestionindependent for the first pass. Converge it only after the event-store write path can use the public event envelope without importing projection-only or database-only concepts. - Remove the public re-exports from
sharedafter all internal consumers have migrated and one compatibility release cycle has passed.
Use the same workspace license and author metadata as intuition-curves.
Before any publish:
- package names are final and checked on crates.io
- README and crate docs explain stable vs. private surfaces
cargo package -p <crate> --listcontains no internal planning filescargo publish -p <crate> --dry-runpassescargo doc -p <crate> --no-depspasses without new broken links- gitleaks scans the release commit and full published history
- Create
intuition-core-primitivesand move scalar/event primitive contracts. Acceptance: projections compiles throughsharedcompatibility re-exports, andcargo publish -p intuition-core-primitives --dry-runpasses. - Create
intuition-core-eventsand move event envelopes, typed records, and parsed-event logic. Acceptance: projections tests pass with imports still compatible, parse behavior unchanged, and package dry-run passes. - Migrate
projectionsimports fromsharedto the new crates. Acceptance: no direct imports of public event/type contracts fromsharedremain, and full Rust CI passes. - Make
sharedservice-local. Acceptance:sharedcontains only config/error/locking/feature-flag support or is renamed to an unpublished service-support crate. - Evaluate
rindexer-ingestionconvergence. Acceptance: written decision on whether ingestion usesintuition-core-eventsfor event-store writes or remains intentionally independent. - Decide whether read-model DTOs deserve
intuition-core-projection-types. Acceptance: schema compatibility policy exists before any publish attempt.
- Do not publish
projectionsas a crate. It is a runtime service distributed as a container image. - Do not expose Redis, SQLx pool, advisory lock, or env-loading helpers as public library APIs.
- Do not force
rindexer-ingestionto depend on the new crates until the write path has a clear compatibility benefit.