Skip to main content
Thirteen questions, thirteen locks. These decisions shape the L0 foundation ( admitted + remaining through Round 4) and the L0.5 kernel contract. Revised twice by swarm audit: current state is rev. 3.
Canonical source: docs/architecture/l0-l05-architecture-decisions.md. Full rationale (3-4 agents per question) lives in the engine repo.

Decision index

QDecisionStatus
Q1No proc macros in L0 · manual impl + macro_rules!LOCKED
Q2No nika-stdx; split nika-error → nika-types + nika-errorLOCKED · executed
Q3Extract nika-catalog-codegen NOW (testable + reusable)LOCKED
Q4nika-event: 3-layer split (L0 types + L1 store + L2 export)LOCKED
Q5Admission order: schema → codegen → event → binding → pck-manifestLOCKED
Q6EventKind = scoped sub-enums (Pattern B, ~22 categories)LOCKED
Q7nika-kernel prelude re-export hub (4 lines, 0 new deps)LOCKED
Q8nika-transform = standalone L0 crate (rev.2: 2 consumers found)LOCKED rev.2
Q9Timestamp + WallDuration = module in nika-typesLOCKED rev.2
Q10Canonical-JSON (RFC 8785) = module in nika-typesLOCKED rev.2
Q11Token-streaming cardinality = delta-batching, not 1-event-per-tokenLOCKED rev.3
Q12Drop ObservabilitySink + add AuditSink (5 sibling channels)LOCKED rev.3 · executed
Q13Bridge OTel GenAI semconv via typed GenAiAttrs on InferLOCKED rev.3 · executed

Why these decisions exist

L0 is the foundation. A wrong choice at L0 propagates through 40+ crates. These decisions were made BEFORE any L1 / L2 admission to avoid costly migrations. Each was validated by ≥3 Rust council agents + cross-checked against 17 Phase-C research agents.

Deep dives

Rationale. Proc macros add compile-time dependency on syn + quote
  • proc-macro2 (~50 MLOC compile graph). L0 crates must compile in seconds for CI parity testing. Manual impl Trait for Struct + macro_rules! covers every pattern we’ve needed at L0.
Escape hatch. Proc macros allowed at L2+ (e.g., #[builtin_tool] attribute macro in nika-verb-invoke). L0 stays procmacro-free forever.Impact. nika-types compile time stays < 2s; downstream crates don’t pay proc-macro cost transitively.
Rationale. A generic nika-stdx crate devolves into a kitchen-sink (anti-pattern #1 from layer-registry). Instead, foundation types live in nika-types (splitable when cohesion demands), error infra in nika-error, lookup tables in nika-catalog. Purpose-named, not kitchen-sink.Executed. nika-types admitted as L0 foundation (no I/O, no async, Cow-heavy). nika-error re-exports the subset it needs.
Rationale. TOML → Rust codegen is testable logic that today lives in build.rs. Extract it so:
  • build.rs remains <50 LOC (delegates).
  • Codegen gets its own unit tests + snapshot tests.
  • Community overlays (nika-catalog-cn) reuse the same codegen.
Targets admission Round 3 post-nika-schema.
Rationale. Event emission (L0 types + scoped sub-enums) is concept separate from storage (L1 nika-event-store writes NDJSON / SQLite) which is separate from export (L2 nika-event-export pushes OTel / Datadog / Honeycomb). Each layer tested in isolation.Mapping: L0 nika-event (~2.5k LOC) types + sub-enums · L1 nika-event-store (~3k LOC, future) · L2 nika-event-export (~2k LOC, future).
Order. schema → catalog-codegen → event → binding → pck-manifest.Why this order.
  • nika-schema unblocks the rest (it defines the AST).
  • catalog-codegen independent (purely build-time).
  • nika-event needs types (done) and schema (for correlation IDs).
  • nika-binding needs event + transform.
  • nika-pck-manifest is standalone and can run in parallel with any of the above.
Rationale. A single EventKind enum with 22 variants becomes a merge-conflict magnet and bloats every consumer’s match arm. Instead, ~22 sub-enums grouped by emission site (ExecEvent, InferEvent, ProviderEvent, etc.), unified under an Event sum type via the event_categories! macro.Impact. Each verb crate emits its own sub-enum variants; consumers match on the category they care about; rest are transparent.
Rationale. L2+ verb crates today use nika_types::*; use nika_error::*; use nika_kernel::traits::*; (3 imports every file). Instead, nika-kernel ships pub mod prelude that re-exports from nika-types
  • nika-error + its own traits. Verb crates: use nika_kernel::prelude::*;.
Cost. 4 lines of code. Zero new deps (already transitive).
Original decision. Inline nika-transform into nika-binding (single consumer).Reversion rationale. Swarm audit found a second consumer: nika-builtin-* crates need nika-transform filters (json, base64, shell_quote) for invoke:builtin: parameter processing. Inlining would force nika-binding dependency on every builtin crate (wrong coupling).Final. nika-transform stays L0, 65 transforms in 7 sub-modules, two consumers (binding + builtin-*).
Rationale rev.2. Both were initially planned as standalone L0 crates (nika-time, nika-canonical). Audit found each has zero non-foundation dependencies and tight coupling to core types. They become modules inside nika-types:
  • nika_types::timestamp: Timestamp + WallDuration (monotonic + RFC3339)
  • nika_types::hash::canonical: RFC 8785 canonical-JSON for hash stability
Impact. nika-types gains ~300 LOC, stays well under 15k crate cap.
Rationale. Emitting 1 InferEvent::Token per LLM token would flood the event bus (2k tokens/s on fast providers). Instead, batch into deltas: emit InferEvent::Delta { text, token_count, duration } every 50ms or 32 tokens, whichever first.Impact. Event rate capped at ~20 Hz regardless of provider speed.
Rationale rev.3. The original ObservabilitySink god-trait would have forced every sink implementation to handle 5 concerns at once. Instead, 5 sibling traits live alongside each other:
  • EventSink: structured events (NDJSON, OpenTelemetry)
  • MetricsExporter: Prometheus / StatsD / OTel metrics
  • TracerProvider: OTel traces (W3C TraceContext)
  • AuditSink: compliance audit log (tamper-evident, separate from EventSink)
  • BillingSink: cost accounting ($ per provider call)
Impact. Each sink tested independently. Implementations can mix-and-match (e.g., Honeycomb for trace, Prometheus for metrics, local NDJSON for audit).
Rationale rev.3. OpenTelemetry defines GenAI semantic conventions for LLM attributes (gen_ai.system, gen_ai.request.model, etc.). Rather than stuff strings into a HashMap<String, String>, expose a typed GenAiAttrs struct on InferRequest / InferResponse.Impact. Consumers get IDE completion + compile-time safety. Non-breaking: GenAiAttrs is #[non_exhaustive]. New attrs are additive.

See also

Layer registry

How these L0 crates fit into the 6-layer pyramid.

Forward-compat invariants

The 8 patterns that make these decisions safe to evolve.

12-gate admission

How each L0 crate earns its seat.

Constellation

Live state: which L0 crates are admitted today.