Skip to content

Commit b4e0ce0

Browse files
lxsaahclaude
andauthored
Feat/enhance data contracts (#170)
* feat(data-contracts): add SimulatableRegistrarExt and ObservableRegistrarExt for enhanced data integration * Implement code changes to enhance functionality and improve performance * feat(data-contracts): Simulatable v2 — compile-time-only sim + .simulate() verb (design 041 §3.1) Reshape Simulatable to the one-verb-per-contract model and make simulation a compile-time fact, not a runtime flag: - Trait: add `type Params`; `simulate(params, previous, rng, timestamp_ms)`. Delete `SimulationConfig` (incl. `enabled`) and `SimulationParams`; add `SimProfile<P>` + off-the-shelf `RandomWalkParams`. - New `SimulatableRegistrarExt::simulate(profile, rng)` installs a source loop over `ctx.time()` (runtime-neutral, single-writer enforced by build()). - Cargo: `simulatable = ["rand", "aimdb-core"]`; `rand` drops `std_rng` (caller-supplied RNG; SmallRng needs no feature). D1 callers: - weather-mesh-common temp/humidity/location impls → `type Params = RandomWalkParams`. - weather-station-beta (std) + gamma (no_std): `sim` feature gates `.simulate()`; default (prod/flash) build reads a hardware source and is rand-free — the canonical §3.1.4 `#[cfg]` sim-to-real pattern. CI: `make check-no-sim` proves each station's production graph carries no `rand` (the tracer) and that `simulatable` is never a default feature; wired into the CI makefile-build job. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Refactor observability features in AimDB - Removed `ICON` and `format_log` from the `Observable` trait, simplifying the interface. - Introduced `SignalGaugeHandle` for managing signal statistics, allowing for last/min/max/mean tracking. - Added `SignalStats` struct to encapsulate signal statistics with atomic operations for thread safety. - Updated `RecordProfilingMetrics` to include signal gauges and their statistics. - Enhanced `RecordMetadata` to optionally include signal statistics when the observability feature is enabled. - Modified `ObservableRegistrarExt` to support signal observation and logging. - Updated various schemas (e.g., `Temperature`, `Humidity`, `DewPoint`, `GpsLocation`) to align with the new observability model. - Adjusted example applications to utilize the new signal gauge functionality for metrics and logging. * feat(data-contracts): LinkableRegistrarExt + #[derive(Linkable)] (design 041 §3.3) Implementing `Linkable` today changed nothing — every example hand-wired `with_deserializer`/`with_serializer` closures. Fix: - `LinkableRegistrarExt::linked_from`/`linked_to` install `.link_from()`/ `.link_to()` with the codec defaulted to `T::from_bytes`/`T::to_bytes`; the raw builders remain the escape hatch for per-link options (QoS, topic providers/resolvers). - `linkable` feature now depends on `aimdb-core` (same wiring as `observable`) for the registrar/connector-builder types the ext needs. - `#[derive(Linkable)]` in `aimdb-derive` emits `serde_json::to_vec`/ `from_slice` — no_std + alloc compatible (serde_json here is alloc-only, no `std` required), removing hand-written JSON boilerplate. D1 caller: tokio-mqtt-connector-demo's TempOutdoor/TempServerRoom (outbound) and CommandKey::TempOutdoor (inbound) now use `#[derive(Linkable)]` + `.linked_to`/`.linked_from`; TempIndoor keeps the raw builder since it needs QoS/retain options the ext doesn't cover. mqtt-connector-demo-common gains an opt-in `data-contracts` feature (alloc-only) so the embassy demo's hand-rolled no_std JSON path stays untouched — it doesn't enable the new feature. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(sync): Settable -> SyncProducer::set_value family (design 041 §3.4) `Settable` was built for the sync bridge, but `aimdb-sync` never referenced it — `SyncProducer::set(value: T)` takes a fully constructed record, so every outside-the-thread caller hand-assembled the struct. - `Settable` moves behind a `settable` feature in `aimdb-data-contracts` for tier symmetry with the other wire contracts (pure trait, no dependencies of its own). weather-mesh-common enables it unconditionally (used by all three contracts); codegen's `generate_cargo_toml`/import emission gained the same `has_settable` detection already used for `has_observable`. - `aimdb-sync` gains a `data-contracts` feature -> optional `aimdb-data-contracts/settable` dep (dependency direction stays contracts -> sync, no `sync` feature added to the contracts crate). `SyncProducer<T: Settable>::set_value`/`try_set_value`/`set_value_at` construct via `T::set(value, timestamp)` and send; `set_value`/ `try_set_value` stamp the caller's `SystemTime`, `set_value_at` takes an explicit timestamp for replay/testing. - Doctest + integration tests (aimdb-sync/tests/settable_integration.rs) exercise `set_value`/`try_set_value`/`set_value_at` end-to-end (construct -> produce -> consume). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: README verb/tier table + CHANGELOG sweep + version bumps (design 041 §6-7) - README: the 4-trait capability table becomes the §2 verb/tier table (Contract / Implement when / Verb / Tier), documents the `Simulatable` dev-tier "never ships in prod" call-out with the `#[cfg]` sim-to-real pattern, and adds the `Settable` row. - CHANGELOG entries for all four touched crates (aimdb-data-contracts, aimdb-core, aimdb-derive, aimdb-sync) describing the trait reshapes, new registrar ext traits, the signal-gauge surface, and the derive macro. - Version bumps: aimdb-data-contracts 0.2.0 -> 0.3.0 (breaking: Simulatable/ Observable reshaped, Settable feature-gated), aimdb-core 1.1.0 -> 1.2.0 (additive: signal_gauge surface), aimdb-derive 0.2.0 -> 0.3.0 (additive: #[derive(Linkable)]), aimdb-sync 0.5.0 -> 0.6.0 (additive: set_value family). Dependents pinning an exact version (aimdb-sync, aimdb-wasm- adapter, aimdb-websocket-connector -> data-contracts; aimdb-core, aimdb-data-contracts -> aimdb-derive) updated to match; aimdb-core's own 0.x-style dependents needed no change (^1.1.0 already permits 1.2.0). Verified: `make check` and `make build` green in aimdb; `make check`/`make all` green in aimdb-pro against the bumped versions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Claude/design doc 41 review s7hguj (#169) * fix(review): SignalStatsInfo serde symmetry, live hub gauge, gamma fmt Review follow-ups on the design 041 implementation: - SignalStatsInfo.unit: add #[serde(default)] to match its skip_serializing_if. Observable::UNIT defaults to "", so a gauge without a unit serialized record.list/record.get JSON that aimdb-client/aimdb-mcp (built with observability) could not deserialize back (missing field `unit`). Regression test added. - weather-hub: enable aimdb-tokio-adapter/observability so .observe() gets a live gauge instead of the inert handle — the demo now actually surfaces signal stats on record.list/record.get as its comments and design 041 §6 claim. - weather-station-gamma: rustfmt the non-sim read_temperature signature (make fmt-check was failing). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017sJzkN61UVQboWuHVHwSKP * ci: make check-no-sim fail closed + tracer positive control The guard treated ANY non-zero 'cargo tree -i rand' exit as 'rand is absent'. An unrelated cargo failure — missing _external submodules, a typo'd package in SIM_EXAMPLES, registry trouble — therefore passed the check vacuously (observed: with submodules uninitialized, all four assertions 'passed' while cargo was erroring on embassy-executor). Now 'rand-free' is accepted only when cargo tree fails with its specific 'did not match any packages' error; any other failure aborts the check and prints cargo's output. A positive control per example additionally asserts the '--features sim' graph DOES find rand, so a silently broken tracer (e.g. rand dropped from the simulatable feature) can no longer turn the whole guard into a no-op. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017sJzkN61UVQboWuHVHwSKP --------- Co-authored-by: Claude <noreply@anthropic.com> * feat(tests): add serde_json as a dev-dependency for roundtrip tests * Refactor documentation and comments across multiple modules - Updated comments in `linkable.rs`, `observable.rs`, and `simulatable.rs` to improve clarity and remove references to design documents. - Enhanced documentation in `lib.rs` of `aimdb-derive` to clarify the derive functionality for `Linkable`. - Revised comments in `Cargo.toml` files across various examples to remove design references and improve readability. - Adjusted comments in `producer.rs` and integration tests to clarify the functionality of `SyncProducer` and its methods. - Updated design documentation to reflect changes in the implementation and clarify the purpose of various traits and features. - Removed unnecessary design references in example projects to streamline the documentation and focus on functionality. * docs: update next development areas section to reference GitHub issues * feat(docs): enhance README with evidence of data contracts and capabilities * refactor(examples): simplify comments in main.rs for clarity * feat(observable): enhance documentation on feature layering and recording behavior * feat(error): improve server error message formatting --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 355c146 commit b4e0ce0

51 files changed

Lines changed: 1931 additions & 598 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ jobs:
7575
- name: Check codegen template drift
7676
run: make codegen-drift
7777

78+
- name: Prove production is simulation-free
79+
run: make check-no-sim
80+
7881
# Embedded target testing
7982
embedded-targets:
8083
name: Embedded Cross-compilation

CONTRIBUTING.md

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -235,12 +235,7 @@ examples/ # Demo applications
235235
236236
## Next Development Areas
237237
238-
See `.github/copilot-instructions.md` for current implementation status and priorities:
239-
240-
- **Kafka Connector** - Kafka integration using `rdkafka`
241-
- **DDS Connector** - DDS protocol support
242-
- **CLI Tools** - Introspection, monitoring, debugging commands
243-
- **Performance** - Benchmarks and profiling infrastructure
238+
See the [GitHub issues](https://github.com/aimdb-dev/aimdb/issues) for planned work and open feature requests.
244239
245240
## Getting Help
246241

Cargo.lock

Lines changed: 7 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Makefile

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# AimDB Makefile
22
# Simple automation for common development tasks
33

4-
.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
4+
.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
55
.DEFAULT_GOAL := help
66

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

558+
# Prove that simulation code (the dev-tier `simulatable` contract) never
559+
# reaches a production binary. `rand` is the tracer: it is reachable iff
560+
# `simulatable` is enabled (aimdb-data-contracts/src/simulatable.rs).
561+
# The guard must not fail open:
562+
# "rand is absent" is accepted only when `cargo tree -i rand` fails with its
563+
# specific "did not match any packages" error — any other failure (missing
564+
# submodule, typo'd package name, registry trouble) aborts the check instead of
565+
# passing it vacuously. A positive control per example asserts the sim build
566+
# DOES find `rand`, proving the tracer still traces. Also asserts `simulatable`
567+
# is not a default feature of the contracts crate (which would pull `rand` into
568+
# its own default graph).
569+
SIM_EXAMPLES := weather-station-beta weather-station-gamma
570+
check-no-sim:
571+
@printf "$(GREEN)Proving production graphs are simulation-free...$(NC)\n"
572+
@tree_rand() { cargo tree -p "$$1" $$2 -e normal -i rand 2>&1; }; \
573+
assert_rand_free() { \
574+
if out=$$(tree_rand "$$1" "$$2"); then \
575+
printf "$(RED)$$3$(NC)\n"; \
576+
exit 1; \
577+
elif ! printf '%s\n' "$$out" | grep -q 'did not match any packages'; then \
578+
printf "$(RED)✗ cargo tree for '$$1' failed for a reason other than 'rand is absent' — refusing to pass vacuously:$(NC)\n"; \
579+
printf '%s\n' "$$out"; \
580+
exit 1; \
581+
fi; \
582+
}; \
583+
for bin in $(SIM_EXAMPLES); do \
584+
assert_rand_free "$$bin" "" "'$$bin' (default/production, no sim) pulls in 'rand' — simulation code leaked into production"; \
585+
printf "$(BLUE)$$bin production graph is rand-free$(NC)\n"; \
586+
if ! tree_rand "$$bin" "--features sim" >/dev/null; then \
587+
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"; \
588+
exit 1; \
589+
fi; \
590+
printf "$(BLUE)$$bin sim graph finds rand (tracer positive control)$(NC)\n"; \
591+
done; \
592+
assert_rand_free "aimdb-data-contracts" "" "aimdb-data-contracts pulls 'rand' with default features — 'simulatable' must never be a default feature"; \
593+
printf "$(BLUE)✓ 'simulatable' is not a default feature of aimdb-data-contracts$(NC)\n"; \
594+
printf "$(GREEN)✓ Production is simulation-free$(NC)\n"
595+
558596
## Convenience commands
559-
check: fmt-check clippy test test-embedded test-wasm deny readme-check codegen-drift
597+
check: fmt-check clippy test test-embedded test-wasm deny readme-check codegen-drift check-no-sim
560598
@printf "$(GREEN)Comprehensive development checks completed!$(NC)\n"
561599
@printf "$(BLUE)✓ Code formatting verified$(NC)\n"
562600
@printf "$(BLUE)✓ Linter passed$(NC)\n"

README.md

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,22 @@ AimDB is not a storage engine. It's a typed data plane where the Rust type *is*
3939

4040
## Why AimDB exists
4141

42-
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.
42+
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.
4343

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

48+
### The receipts
49+
50+
None of this is roadmap. Every claim is backed by code, tests or committed benchmarks:
51+
52+
- **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.
53+
- **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.
54+
- **Zero allocation per message**[allocation baselines](aimdb-bench/data/baselines) across Tokio, Embassy and WASM.
55+
- **Non-blocking producers** → synchronous [`produce()`](aimdb-core/src/typed_api.rs) calls and overwrite semantics on all buffers.
56+
- **Identical buffer contracts across runtimes**[shared conformance suite](aimdb-core/src/buffer/test_support.rs) validates SPMC Ring, SingleLatest and Mailbox on every adapter.
57+
4858
[The Next Era of Software Architecture Is Data-First](https://aimdb.dev/blog/data-driven-design)
4959

5060
---
@@ -93,10 +103,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
93103
});
94104
});
95105

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

131-
**Four capability traits** — opt-in, type-checked:
138+
**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)
132139

133-
| Trait | What it unlocks | Runtimes |
134-
| --- | --- | --- |
135-
| [`Streamable`](https://aimdb.dev/blog/streamable-crossing-boundaries) | Crossing WASM / WebSocket / CLI boundaries | std, no_std |
136-
| [`Migratable`](https://aimdb.dev/blog/schema-migration-without-ceremony) | Typed schema evolution across deployed fleets | std, no_std |
137-
| `Observable` | Automatic per-record metrics | std, no_std |
138-
| [`Linkable`](https://aimdb.dev/blog/connectors-where-aimdb-meets-the-real-world) | Wire-format connectors | std, no_std |
140+
**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)
139141

140-
**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)
142+
**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.
143+
144+
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)
145+
146+
---
141147

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

144-
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)
150+
Every capability is an opt-in trait on your schema type: implement it and **exactly one method** appears on the registrar.
151+
152+
| Contract | Implement when… | Verb it unlocks | Tier |
153+
| --- | --- | --- | --- |
154+
| [`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) |
155+
| [`Streamable`](https://aimdb.dev/blog/streamable-crossing-boundaries) | the record streams to browsers as schema-named JSON | ws-connector `.register::<T>()` | wire (prod) |
156+
| [`Migratable`](https://aimdb.dev/blog/schema-migration-without-ceremony) | the schema evolved across versions | `migration_chain!` | wire (prod) |
157+
| `Settable` | sync code outside AimDB sets the value | `SyncProducer::set_value(v)` | wire (prod) |
158+
| `Observable` | the value is worth watching in production | `.observe()` → live last/min/max/mean on `record.list`/`record.get` | introspection (prod, optional) |
159+
| `Simulatable` | the type can generate realistic synthetic data | `.simulate(profile, rng)` | **dev-only — never ships in prod** |
160+
161+
`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:
162+
163+
```rust
164+
builder.configure::<Temperature>(KEY, |reg| {
165+
#[cfg(feature = "sim")]
166+
reg.simulate(profile, rng);
167+
#[cfg(not(feature = "sim"))]
168+
reg.source(read_hardware);
169+
});
170+
```
171+
172+
Build with `sim` off and the simulation code is *gone* — no `T::simulate` impls, no `rand`, nothing to audit.
173+
174+
**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.
175+
176+
Deep dive: [data contracts](https://aimdb.dev/blog/data-contracts-deep-dive)
145177

146178
---
147179

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

163-
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.
195+
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.
164196

165197
---
166198

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

246+
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.
247+
214248
---
215249

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

229263
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:
230264

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

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

aimdb-codegen/src/rust.rs

Lines changed: 10 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,18 @@ pub fn generate_cargo_toml(state: &ArchitectureState) -> String {
132132
.iter()
133133
.any(|r| r.serialization.as_ref() == Some(&SerializationType::Postcard));
134134
let has_observable = state.records.iter().any(|r| r.observable.is_some());
135+
let has_settable = state
136+
.records
137+
.iter()
138+
.any(|r| r.fields.iter().any(|f| f.settable));
135139

136140
let mut data_contracts_features = Vec::new();
137141
if has_non_custom_ser {
138142
data_contracts_features.push("\"linkable\"");
139143
}
144+
if has_settable {
145+
data_contracts_features.push("\"settable\"");
146+
}
140147

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

1012-
let icon = &obs.icon;
10131019
let unit = &obs.unit;
10141020

1015-
// Timestamp heuristic: first u64 field named timestamp/computed_at/fetched_at
1016-
let timestamp_names = ["timestamp", "computed_at", "fetched_at"];
1017-
let timestamp_field = rec
1018-
.fields
1019-
.iter()
1020-
.find(|f| f.field_type == "u64" && timestamp_names.contains(&f.name.as_str()));
1021-
1022-
let format_log_body = if let Some(ts) = timestamp_field {
1023-
let ts_ident = format_ident!("{}", ts.name);
1024-
quote! {
1025-
alloc::format!(
1026-
"{} [{}] {}: {:.1}{} at {}",
1027-
Self::ICON,
1028-
node_id,
1029-
Self::NAME,
1030-
self.signal(),
1031-
Self::UNIT,
1032-
self.#ts_ident,
1033-
)
1034-
}
1035-
} else {
1036-
quote! {
1037-
alloc::format!(
1038-
"{} [{}] {}: {:.1}{}",
1039-
Self::ICON,
1040-
node_id,
1041-
Self::NAME,
1042-
self.signal(),
1043-
Self::UNIT,
1044-
)
1045-
}
1046-
};
1047-
1021+
// Observable is a kernel-only trait: the numeric projection + UNIT label.
1022+
// `SIGNAL` defaults to the schema name; presentation (icons, log formatting)
1023+
// is not part of the trait, so nothing else is emitted.
10481024
Some(quote! {
10491025
impl Observable for #struct_name {
10501026
type Signal = #signal_type;
1051-
const ICON: &'static str = #icon;
10521027
const UNIT: &'static str = #unit;
10531028

10541029
fn signal(&self) -> #signal_type {
10551030
self.#signal_ident
10561031
}
1057-
1058-
fn format_log(&self, node_id: &str) -> alloc::string::String {
1059-
#format_log_body
1060-
}
10611032
}
10621033
})
10631034
}

aimdb-core/CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- **`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`).
13+
1014
### Fixed
1115

1216
- **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()`.
@@ -452,7 +456,8 @@ warning fires if you exceed 1000 interned keys.
452456

453457
---
454458

455-
[Unreleased]: https://github.com/aimdb-dev/aimdb/compare/v1.1.0...HEAD
459+
[Unreleased]: https://github.com/aimdb-dev/aimdb/compare/v1.2.0...HEAD
460+
[1.2.0]: https://github.com/aimdb-dev/aimdb/compare/v1.1.0...v1.2.0
456461
[1.1.0]: https://github.com/aimdb-dev/aimdb/compare/v1.0.0...v1.1.0
457462
[1.0.0]: https://github.com/aimdb-dev/aimdb/compare/v0.5.0...v1.0.0
458463
[0.5.0]: https://github.com/aimdb-dev/aimdb/compare/v0.4.0...v0.5.0

0 commit comments

Comments
 (0)