Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
18f85e0
feat(data-contracts): add SimulatableRegistrarExt and ObservableRegis…
lxsaah Jul 6, 2026
33e9692
Implement code changes to enhance functionality and improve performance
lxsaah Jul 6, 2026
497f964
feat(data-contracts): Simulatable v2 — compile-time-only sim + .simul…
lxsaah Jul 6, 2026
be4a156
Refactor observability features in AimDB
lxsaah Jul 6, 2026
e226a31
feat(data-contracts): LinkableRegistrarExt + #[derive(Linkable)] (des…
lxsaah Jul 7, 2026
55b7d38
feat(sync): Settable -> SyncProducer::set_value family (design 041 §3.4)
lxsaah Jul 7, 2026
bbc4477
docs: README verb/tier table + CHANGELOG sweep + version bumps (desig…
lxsaah Jul 7, 2026
a874372
Claude/design doc 41 review s7hguj (#169)
lxsaah Jul 7, 2026
43e0992
Merge branch 'main' into feat/enhance-data-contracts
lxsaah Jul 7, 2026
7dc703e
feat(tests): add serde_json as a dev-dependency for roundtrip tests
lxsaah Jul 7, 2026
145d321
Refactor documentation and comments across multiple modules
lxsaah Jul 7, 2026
39e2a4d
docs: update next development areas section to reference GitHub issues
lxsaah Jul 7, 2026
219b709
feat(docs): enhance README with evidence of data contracts and capabi…
lxsaah Jul 7, 2026
acb842f
refactor(examples): simplify comments in main.rs for clarity
lxsaah Jul 8, 2026
d6a66bc
feat(observable): enhance documentation on feature layering and recor…
lxsaah Jul 8, 2026
c56a362
Merge branch 'main' into feat/enhance-data-contracts
lxsaah Jul 9, 2026
a283910
feat(error): improve server error message formatting
lxsaah Jul 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ jobs:
- name: Check codegen template drift
run: make codegen-drift

- name: Prove production is simulation-free
run: make check-no-sim

# Embedded target testing
embedded-targets:
name: Embedded Cross-compilation
Expand Down
7 changes: 1 addition & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,12 +235,7 @@ examples/ # Demo applications

## Next Development Areas

See `.github/copilot-instructions.md` for current implementation status and priorities:

- **Kafka Connector** - Kafka integration using `rdkafka`
- **DDS Connector** - DDS protocol support
- **CLI Tools** - Introspection, monitoring, debugging commands
- **Performance** - Benchmarks and profiling infrastructure
See the [GitHub issues](https://github.com/aimdb-dev/aimdb/issues) for planned work and open feature requests.

## Getting Help

Expand Down
12 changes: 7 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 40 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# AimDB Makefile
# Simple automation for common development tasks

.PHONY: help build test clean clean-embedded fmt fmt-check clippy doc all check test-embedded test-wasm wasm wasm-test examples deny audit security publish publish-check readme-check codegen-drift
.PHONY: help build test clean clean-embedded fmt fmt-check clippy doc all check test-embedded test-wasm wasm wasm-test examples deny audit security publish publish-check readme-check codegen-drift check-no-sim
.DEFAULT_GOAL := help

# Separate target dir for embedded checks so an interrupted example build
Expand Down Expand Up @@ -555,8 +555,46 @@ codegen-drift:
@printf "$(GREEN)Checking codegen templates against the workspace API...$(NC)\n"
./tools/scripts/codegen-drift-check.sh

# Prove that simulation code (the dev-tier `simulatable` contract) never
# reaches a production binary. `rand` is the tracer: it is reachable iff
# `simulatable` is enabled (aimdb-data-contracts/src/simulatable.rs).
# The guard must not fail open:
# "rand is absent" is accepted only when `cargo tree -i rand` fails with its
# specific "did not match any packages" error — any other failure (missing
# submodule, typo'd package name, registry trouble) aborts the check instead of
# passing it vacuously. A positive control per example asserts the sim build
# DOES find `rand`, proving the tracer still traces. Also asserts `simulatable`
# is not a default feature of the contracts crate (which would pull `rand` into
# its own default graph).
SIM_EXAMPLES := weather-station-beta weather-station-gamma
check-no-sim:
@printf "$(GREEN)Proving production graphs are simulation-free...$(NC)\n"
@tree_rand() { cargo tree -p "$$1" $$2 -e normal -i rand 2>&1; }; \
assert_rand_free() { \
if out=$$(tree_rand "$$1" "$$2"); then \
printf "$(RED)✗ $$3$(NC)\n"; \
exit 1; \
elif ! printf '%s\n' "$$out" | grep -q 'did not match any packages'; then \
printf "$(RED)✗ cargo tree for '$$1' failed for a reason other than 'rand is absent' — refusing to pass vacuously:$(NC)\n"; \
printf '%s\n' "$$out"; \
exit 1; \
fi; \
}; \
for bin in $(SIM_EXAMPLES); do \
assert_rand_free "$$bin" "" "'$$bin' (default/production, no sim) pulls in 'rand' — simulation code leaked into production"; \
printf "$(BLUE)✓ $$bin production graph is rand-free$(NC)\n"; \
if ! tree_rand "$$bin" "--features sim" >/dev/null; then \
printf "$(RED)✗ positive control failed: '$$bin --features sim' does not pull 'rand' — the tracer no longer traces, so the rand-free results above prove nothing$(NC)\n"; \
exit 1; \
fi; \
printf "$(BLUE)✓ $$bin sim graph finds rand (tracer positive control)$(NC)\n"; \
done; \
assert_rand_free "aimdb-data-contracts" "" "aimdb-data-contracts pulls 'rand' with default features — 'simulatable' must never be a default feature"; \
printf "$(BLUE)✓ 'simulatable' is not a default feature of aimdb-data-contracts$(NC)\n"; \
printf "$(GREEN)✓ Production is simulation-free$(NC)\n"

## Convenience commands
check: fmt-check clippy test test-embedded test-wasm deny readme-check codegen-drift
check: fmt-check clippy test test-embedded test-wasm deny readme-check codegen-drift check-no-sim
@printf "$(GREEN)Comprehensive development checks completed!$(NC)\n"
@printf "$(BLUE)✓ Code formatting verified$(NC)\n"
@printf "$(BLUE)✓ Linter passed$(NC)\n"
Expand Down
76 changes: 52 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,22 @@ AimDB is not a storage engine. It's a typed data plane where the Rust type *is*

## Why AimDB exists

Distributed systems spend most of their complexity budget translating between layers. IDLs, codegen, serialization, schema registries and glue services. AimDB removes that layer by making **the Rust type the contract**: defined once, compiled unchanged from a `no_std` microcontroller to the browser.
Most of a distributed system's complexity is translation: IDLs, codegen, serialization glue, schema registries. AimDB deletes that layer by making **the Rust type the contract** defined once, compiled unchanged from a `no_std` microcontroller to the browser.

- **One type, every tier.** The same struct compiles for firmware and cloud. No conversion layer between them.
- **The buffer defines how data moves.** No manual queue wiring, no separate transport config.
- **No untyped boundaries.** Capabilities, like streaming, migration, observability and connectors, are unlocked by traits.

### The receipts

None of this is roadmap. Every claim is backed by code, tests or committed benchmarks:

- **Data contracts as schema** → the Rust type *is* the wire contract. No IDL, no codegen, [CI cross-compiles](Makefile) unchanged from Cortex-M to WASM.
- **Typed data migrations** → [`migration_chain!`](aimdb-data-contracts/src/migratable.rs) const-validates the chain and works `no_std`, so old and new nodes coexist on the wire.
- **Zero allocation per message** → [allocation baselines](aimdb-bench/data/baselines) across Tokio, Embassy and WASM.
- **Non-blocking producers** → synchronous [`produce()`](aimdb-core/src/typed_api.rs) calls and overwrite semantics on all buffers.
- **Identical buffer contracts across runtimes** → [shared conformance suite](aimdb-core/src/buffer/test_support.rs) validates SPMC Ring, SingleLatest and Mailbox on every adapter.

[The Next Era of Software Architecture Is Data-First](https://aimdb.dev/blog/data-driven-design)

---
Expand Down Expand Up @@ -93,10 +103,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
});
});

// `.run()` builds the database, collects every producer/consumer/transform
// future, and drives them all on a single `FuturesUnordered`. It blocks
// until shutdown. For programmatic access to the `AimDb` handle, call
// `.build().await?` directly — it returns `(AimDb, AimDbRunner)`.
// Build the db and drive every source/tap future until shutdown.
builder.run().await?;
Ok(())
}
Expand Down Expand Up @@ -128,20 +135,45 @@ docker compose up
| [**SingleLatest**](examples/hello-single-latest-async) | Only the current value matters | Feature flags, config, UI state |
| [**Mailbox**](examples/hello-mailbox) / [**async Mailbox**](examples/hello-mailbox-async)| Latest instruction wins | Device commands, actuation, RPC |

**Four capability traits** — opt-in, type-checked:
**One async API across runtimes.** Tokio, Embassy, WASM — swap the runtime adapter, keep the code. → [How the runtime abstraction works](https://aimdb.dev/blog/building-aimdb-one-async-api)

| Trait | What it unlocks | Runtimes |
| --- | --- | --- |
| [`Streamable`](https://aimdb.dev/blog/streamable-crossing-boundaries) | Crossing WASM / WebSocket / CLI boundaries | std, no_std |
| [`Migratable`](https://aimdb.dev/blog/schema-migration-without-ceremony) | Typed schema evolution across deployed fleets | std, no_std |
| `Observable` | Automatic per-record metrics | std, no_std |
| [`Linkable`](https://aimdb.dev/blog/connectors-where-aimdb-meets-the-real-world) | Wire-format connectors | std, no_std |
**Connectors that ship today:** MQTT, KNX, WebSocket, TCP, serial (COBS-framed UART) and Unix domain sockets. Writing your own is one trait impl. → [Connector status](#connectors)

**One async API across runtimes.** Tokio, Embassy, WASM — swap the runtime adapter, keep the code. → [How the runtime abstraction works](https://aimdb.dev/blog/building-aimdb-one-async-api)
**Optional persistence.** The core is an in-memory data plane; [`aimdb-persistence`](aimdb-persistence) adds `.persist()` with a SQLite backend ([`aimdb-persistence-sqlite`](aimdb-persistence-sqlite)) for records whose history must survive restarts.

Deep dives: [source/tap/transform](https://aimdb.dev/blog/source-tap-transform) · [schema migration](https://aimdb.dev/blog/schema-migration-without-ceremony) · [reactive pipelines](https://aimdb.dev/blog/reactive-pipelines)

---

**Connectors that ship today:** MQTT, KNX, WebSocket. Writing your own is one trait impl.
## Data contracts: one method per capability

Deep dives: [data contracts](https://aimdb.dev/blog/data-contracts-deep-dive) · [source/tap/transform](https://aimdb.dev/blog/source-tap-transform) · [schema migration](https://aimdb.dev/blog/schema-migration-without-ceremony) · [reactive pipelines](https://aimdb.dev/blog/reactive-pipelines)
Every capability is an opt-in trait on your schema type: implement it and **exactly one method** appears on the registrar.

| Contract | Implement when… | Verb it unlocks | Tier |
| --- | --- | --- | --- |
| [`Linkable`](https://aimdb.dev/blog/connectors-where-aimdb-meets-the-real-world) | the record is mirrored to/from an endpoint (MQTT, KNX, serial, UDS…) | `.linked_from(url)` / `.linked_to(url)` (`#[derive(Linkable)]` for JSON) | wire (prod) |
| [`Streamable`](https://aimdb.dev/blog/streamable-crossing-boundaries) | the record streams to browsers as schema-named JSON | ws-connector `.register::<T>()` | wire (prod) |
| [`Migratable`](https://aimdb.dev/blog/schema-migration-without-ceremony) | the schema evolved across versions | `migration_chain!` | wire (prod) |
| `Settable` | sync code outside AimDB sets the value | `SyncProducer::set_value(v)` | wire (prod) |
| `Observable` | the value is worth watching in production | `.observe()` → live last/min/max/mean on `record.list`/`record.get` | introspection (prod, optional) |
| `Simulatable` | the type can generate realistic synthetic data | `.simulate(profile, rng)` | **dev-only — never ships in prod** |

`Simulatable` is the odd one out: it lives behind the `simulatable` feature (never a default) and switching from simulated to real data is one `#[cfg]` in your app:

```rust
builder.configure::<Temperature>(KEY, |reg| {
#[cfg(feature = "sim")]
reg.simulate(profile, rng);
#[cfg(not(feature = "sim"))]
reg.source(read_hardware);
});
```

Build with `sim` off and the simulation code is *gone* — no `T::simulate` impls, no `rand`, nothing to audit.

**Old and new nodes coexist.** Migration steps are typed and bidirectional and `migration_chain!` checks the whole chain at compile time — [roundtrip tests](aimdb-data-contracts/tests/migration_roundtrip.rs) cover upgrade *and* downgrade across multi-step chains. It's `no_std` too: even an MCU can accept a payload one schema version behind or downgrade its output for an older peer.

Deep dive: [data contracts](https://aimdb.dev/blog/data-contracts-deep-dive)

---

Expand All @@ -157,10 +189,10 @@ A record is written by a `Source`, lands in a typed `Buffer` and fans out to in-
Source ───► │ Buffer │
(typed) │ SPMC / SL / │ ───► Tap (another subscriber)
│ Mailbox │
└──────────────┘ ───► Link ──► MQTT / KNX / WebSocket
└──────────────┘ ───► Link ──► MQTT / KNX / WS / UDS / serial
```

The Rust type system enforces correctness at compile time, buffer semantics enforce flow guarantees at runtime and connectors wire to your infrastructure without an integration layer. The same code compiles for MCU, edge, cloud or browser — see [Platform Support](#platform-support) below.
Types check correctness at compile time, buffers enforce flow semantics at runtime and connectors bridge to your infrastructure, with no integration layer in between. The same code compiles for MCU, edge, cloud or browser — see [Platform Support](#platform-support) below.

---

Expand Down Expand Up @@ -211,6 +243,8 @@ See the [MCP server docs](tools/aimdb-mcp/) for Claude Desktop and other editors
| **Kafka** | 📋 Planned | std |
| **Modbus** | 📋 Planned | std, no_std |

The serial, TCP and UDS connectors carry both record mirroring and the AimX remote-access protocol used by the CLI and MCP server. Typed records over multiple transports, from bare metal to cloud.

---

### Platform Support
Expand All @@ -228,14 +262,8 @@ See the [MCP server docs](tools/aimdb-mcp/) for Claude Desktop and other editors

We're a small team building something ambitious. The fastest way to help is to take on a scoped piece of it. Each of these is sized for a few hours and includes file pointers, acceptance criteria and a place to ask questions:

- [#92 — `no_std` `Display` for `DbError` should include numeric fields](https://github.com/aimdb-dev/aimdb/issues/92) · 2–3h · core · embedded
- [#93 — Minimal example: `hello-single-latest`](https://github.com/aimdb-dev/aimdb/issues/93) · 2–3h · docs
- [#95 — CLI: add `aimdb instance ping` subcommand](https://github.com/aimdb-dev/aimdb/issues/95) · 3–4h · cli
- [#96 — CI: fail on broken rustdoc links](https://github.com/aimdb-dev/aimdb/issues/96) · 1–2h · docs
- [#97 — Doctests for `BufferCfg` variants](https://github.com/aimdb-dev/aimdb/issues/97) · 2–3h · core · docs
- [#99 — Async example: `hello-mailbox-async`](https://github.com/aimdb-dev/aimdb/issues/99) · 2–3h · docs
- [#100 — Async example: `hello-single-latest-async`](https://github.com/aimdb-dev/aimdb/issues/100) · 2–3h · docs
- [#101 — Async example: `hello-spmc-ring-async`](https://github.com/aimdb-dev/aimdb/issues/101) · 2–3h · docs
- [#101 — Minimal example: `hello-spmc-ring-async`](https://github.com/aimdb-dev/aimdb/issues/101) · 2–3h · docs

[See all good first issues →](https://github.com/aimdb-dev/aimdb/labels/good%20first%20issue)

Expand Down
49 changes: 10 additions & 39 deletions aimdb-codegen/src/rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,18 @@ pub fn generate_cargo_toml(state: &ArchitectureState) -> String {
.iter()
.any(|r| r.serialization.as_ref() == Some(&SerializationType::Postcard));
let has_observable = state.records.iter().any(|r| r.observable.is_some());
let has_settable = state
.records
.iter()
.any(|r| r.fields.iter().any(|f| f.settable));

let mut data_contracts_features = Vec::new();
if has_non_custom_ser {
data_contracts_features.push("\"linkable\"");
}
if has_settable {
data_contracts_features.push("\"settable\"");
}

let dc_features_str = if data_contracts_features.is_empty() {
String::new()
Expand Down Expand Up @@ -1009,55 +1016,19 @@ fn emit_observable_impl(rec: &RecordDef) -> Option<TokenStream> {
let signal_type: syn::Type = syn::parse_str(&signal_field.field_type).ok()?;
let signal_ident = format_ident!("{}", obs.signal_field);

let icon = &obs.icon;
let unit = &obs.unit;

// Timestamp heuristic: first u64 field named timestamp/computed_at/fetched_at
let timestamp_names = ["timestamp", "computed_at", "fetched_at"];
let timestamp_field = rec
.fields
.iter()
.find(|f| f.field_type == "u64" && timestamp_names.contains(&f.name.as_str()));

let format_log_body = if let Some(ts) = timestamp_field {
let ts_ident = format_ident!("{}", ts.name);
quote! {
alloc::format!(
"{} [{}] {}: {:.1}{} at {}",
Self::ICON,
node_id,
Self::NAME,
self.signal(),
Self::UNIT,
self.#ts_ident,
)
}
} else {
quote! {
alloc::format!(
"{} [{}] {}: {:.1}{}",
Self::ICON,
node_id,
Self::NAME,
self.signal(),
Self::UNIT,
)
}
};

// Observable is a kernel-only trait: the numeric projection + UNIT label.
// `SIGNAL` defaults to the schema name; presentation (icons, log formatting)
// is not part of the trait, so nothing else is emitted.
Some(quote! {
impl Observable for #struct_name {
type Signal = #signal_type;
const ICON: &'static str = #icon;
const UNIT: &'static str = #unit;

fn signal(&self) -> #signal_type {
self.#signal_ident
}

fn format_log(&self, node_id: &str) -> alloc::string::String {
#format_log_body
}
}
})
}
Expand Down
7 changes: 6 additions & 1 deletion aimdb-core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **`RecordRegistrar::signal_gauge(name, unit) -> SignalGaugeHandle`** (design 041 §3.2) — the core hook behind `aimdb-data-contracts`'s `Observable::observe()`. Feeding values via `SignalGaugeHandle::update(f64)` folds them into per-record `SignalStats` (last/min/max/mean), surfaced through `RecordMetadata::signal_stats` on `record.list`/`record.get`. Mirrors the `with_name` precedent: always callable, an inert no-op handle when the `observability` feature is off, so callers never `#[cfg]` on core's features. New public types: `SignalGaugeHandle` (always available), `SignalStats`/`SignalGauge`/`SignalStatsInfo` (feature `observability`).

### Fixed

- **AimX protocol doc rot cleaned up; `remote::PROTOCOL_VERSION` corrected to `"2.0"` and exported.** The `remote` module docs claimed "AimX v1" and linked a spec file that no longer exists; they now describe the v2 NDJSON tagged-frame wire and point at `crate::session::aimx` / `docs/design/remote-access-via-connectors.md`. The AimX dispatch's Welcome uses the constant instead of a hardcoded `"2.0"` (same bytes on the wire). The dead, never-exported v1 `Message` untagged envelope and its helpers were removed from `remote::protocol`. Also de-advertised Kafka/HTTP connector semantics from `ConnectorUrl` docs (the parser is scheme-agnostic; those connectors never existed) and updated the `connector` module docs from the removed `.link()` API to `.link_to()`/`.link_from()`.
Expand Down Expand Up @@ -452,7 +456,8 @@ warning fires if you exceed 1000 interned keys.

---

[Unreleased]: https://github.com/aimdb-dev/aimdb/compare/v1.1.0...HEAD
[Unreleased]: https://github.com/aimdb-dev/aimdb/compare/v1.2.0...HEAD
[1.2.0]: https://github.com/aimdb-dev/aimdb/compare/v1.1.0...v1.2.0
[1.1.0]: https://github.com/aimdb-dev/aimdb/compare/v1.0.0...v1.1.0
[1.0.0]: https://github.com/aimdb-dev/aimdb/compare/v0.5.0...v1.0.0
[0.5.0]: https://github.com/aimdb-dev/aimdb/compare/v0.4.0...v0.5.0
Expand Down
Loading