# kern v0.6.0: Decimal, durable event log, LLM resilience

> The hardening release. Six items moved from the v1.0 list to shipped.

Source: https://kern-lang.eu/blog/v060-release

April 25, 2026

# kern v0.6.0: Decimal, durable event log, LLM resilience

The hardening release. Six items moved from the v1.0 list to shipped, including a real `Decimal` type that unblocks finance and government deployment, a per-provider circuit breaker on the LLM proxy, and an event log that survives crashes and concurrent writers.

## At a glance

Tests, 100% pass

871

Stdlib modules

83

Compiler lines (Kern)

23K

C runtime lines

27K

## A note on the version

Earlier drafts of this changelog labelled the recent feature wave `v1.0.0` and `v1.1.0`. That was premature. **v1.0 is an API stability promise we are not yet ready to honour** — generics are still type-erased, async codegen has synchronous fallback paths, the package manager is still Python, and the foundation is in formation rather than incorporated. Those entries have been consolidated into v0.6.0\. No code was reverted. Only the version label was corrected.

See the [ROADMAP](https://codeberg.org/PUAGroup/kern-lang/src/branch/main/ROADMAP.md) for the explicit v1.0 exit criteria. Pre-1.0 versions are bumped on real progress, not to look further along than we are.

## Decimal — exact base-10 arithmetic

IEEE-754 floats are not acceptable for currency, taxation, or regulatory reporting. So `std.decimal` now exposes a real `Decimal` struct on top of the runtime's arbitrary-precision integers. Parse from a string, format with a locale, round-trip through JSON, never lose a cent.

```
import std.decimal as dec

fn main():
    subtotal = dec.parse("1234.56")?
    vat_rate = dec.parse("0.21")?

    vat   = subtotal.mul(vat_rate).round(2, dec.HALF_EVEN)
    total = subtotal.add(vat)

    # Locale-aware formatting
    print(total.format_currency("€", dec.DE_DE))
    # => "€ 1.493,82"

    # Lossless JSON round-trip via the safe-string form
    j = total.to_json_string()
    back = dec.from_json_string(j)?
    assert(total.cmp(back) == 0)
```

Six rounding modes — `HALF_EVEN` (banker's rounding), `HALF_UP`, `HALF_DOWN`, `CEILING`, `FLOOR`, `DOWN`, `UP` — plus locale formatting for `en_US`, `de_DE`, `nl_NL`, `fr_FR`, and currency formatting for €, $, £. The legacy fixed-point cents helpers are kept for backwards compatibility.

> **This unblocks finance and government deployment.** `std.compliance` now requires `Decimal` rather than `float` for monetary fields — auditors stop asking why your tax calculation rounds differently on different machines.

## LLM proxy and Qdrant — circuit breaker, retry, backoff

Calling someone else's network service has three failure modes that a one-shot HTTP call ignores: transient blips you should retry, persistent outages you should not hammer, and malformed responses that should be rejected before parsing. v0.6.0 wraps both `kern_llm_api_call` and `kern_qdrant_request` in the same resilience layer:

- **Per-provider circuit breaker** — opens after 5 consecutive failures, stays open for 30 seconds, then admits a single half-open probe before closing again
- **Bounded retry** — three attempts maximum, exponential backoff with jitter, no retry storms
- **JSON sanity-check** — response body is validated before parsing, malformed bodies are rejected with a clear error
- **Thread-safe** — circuit state is held in C11 atomics, no global lock contention

```
from std.cap_ai import ai_root, ai_complete
from std.untrusted import untrusted_validate_length

# Model output is Untrusted<str>: validate before use.
# EU AI Act audit logging is automatic.

fn answer(ai: Cap<Ai>, question: str) -> Result<str>:
    reply = ai_complete(ai, "mistral", question)
    return untrusted_validate_length(reply, 1, 4000)

fn main():
    ai = ai_root()
    result = answer(ai, "How do I deploy?")
    print(result)
```

Both calls above route through the same retry/backoff/circuit-breaker path. When Qdrant blips, your service waits 250 ms and tries again. When Mistral has a sustained outage, your service stops hammering it and reports the error in 3 seconds, not 30\. EU AI Act audit logging is mandatory per call and runs after the resilient call succeeds — every successful inference lands in the audit log.

## Event log — flock, fsync, chain re-verification

The BLAKE2b chained event log was the right primitive but the wrong durability story: no `flock` meant two processes could interleave appends, no `fsync` meant a crash could lose entries, and the chain was only verified by tooling, not by the writer itself. v0.6.0 closes all three:

- `kern_eventlog_open` calls `kern_eventlog_verify` first — it refuses to open a tampered chain
- `kern_eventlog_append` takes an exclusive `flock(2)` for the duration of the write
- The log fsyncs every 16 appends by default; tune with `eventlog_set_fsync_interval` or force with `eventlog_sync`
- `eventlog_close` always fsyncs

```
import std.audit as audit

fn on_user_export(user_id: int, requester: str):
    log = audit.open("/var/log/kern/audit.jsonl")?
    # open() refuses if the BLAKE2b chain is broken

    log.append({
        "event":     "gdpr_export",
        "user_id":   user_id,
        "requester": requester,
        "article":   "GDPR Art. 15",
    })
    # flock held for the duration of the write,
    # fsync triggered every 16 appends

    log.close()  # always fsyncs
```

## SSE production polish

Server-Sent Events that work on a developer laptop tend to fail on the way through a reverse proxy: idle connections get reaped, reconnecting clients can't tell the server what they last saw. v0.6.0 ships the two missing pieces:

- `sse_send_heartbeat` emits a comment frame to keep idle connections alive across proxies
- `sse_send_event_id` writes the `id:` line so reconnecting clients can resume via `Last-Event-ID`

## net.tls — passive audit against BSI TR-02102-2

The new `net.tls` module classifies every TLS connection against the German Federal Office for Information Security's **TR-02102-2** cipher-suite recommendations. TLS version validation, cipher suite classification (forward-secrecy, AEAD, BSI-approved), connection audit reports, and a strict policy mode that requires TLS 1.3 + AEAD + FS.

For a Dutch _or_ German public-sector deployment, this is the difference between "we use TLS" and "we use TLS in a configuration the regulator has explicitly endorsed."

## std.compliance — explicit article-level mapping

Compliance is not "encryption + audit log + good intentions." It is a specific set of articles in a specific set of regulations. `std.compliance` now maps every runtime operation — collect, store, process, transfer, delete, access, encrypt — to its specific article in **GDPR Art. 5–44**, **DORA Art. 5/9/10/11**, and **NIS2 Art. 21/23**. With consent validation, data classification (standard / sensitive / special-category), cross-border transfer checks (adequacy / SCCs), retention recommendations, and audit entries that name the article they satisfy.

Auditor-readable, not lawyer-readable.

## Other stdlib additions

- **std.crypto attack-aware primitives** — nonce reuse detection (`encrypt_tracked`), nonce registry, key strength validation, constant-time secure compare, domain-separated hashing
- **std.graph** — directed/undirected graphs with integer weights; Dijkstra, BFS, DFS, edge/node queries
- **std.observe** — OODA-based latency monitoring; response-time measurement, latency classification, route-level metrics, degradation detection
- **std.ai.feedback** — generate-test-improve loops with integer scoring (0–100), prompt refinement with iteration feedback, few-shot injection, convergence detection

## Compiler hardening

- **Parser recursion-depth limit** — `parse_block` now caps at 256 (matching `parse_expr`); pathologically deep `if`/`while`/`loop` nesting returns a parser diagnostic instead of stack-overflowing the compiler
- **PersonalData → AI test coverage** — added 10 conformance + 5 negative tests covering `pd_anonymize` / `pd_access` / aliasing / string concat taint / `llm_chat` / `llm_api_call(prompt)` / `llm_api_call(system)`. The flagship privacy guard is now a load-bearing test set, not a single check.
- **Generics strategy committed** — hybrid: monomorphization for stdlib containers, `Decimal`, `Result`, `Option`; boxed RTTI for user generics by default; `@monomorphize` attribute to opt in. See `docs/design/generics-strategy.md` for the rationale.
- **Async codegen — explicit TODO + design doc** — the gap (`Await`/`Spawn` currently fall through to synchronous evaluation in some branches) is now documented in `docs/design/async-codegen.md` with the v1.0 plan

## Why this is v0.6.0 and not v1.0.0

v1.0 is a contract: no breaking changes without a v2\. That contract requires the known ABI-breaking decisions made _before_ the promise, the flagship safety claims covered by enough tests that a regression is impossible to land silently, and production primitives hardened with retry, backoff, fsync, and reconnect — not one-shot best-effort. We aren't there yet.

The remaining gates:

- **Generics** are type-erased to `i8*`. The hybrid strategy is committed; the implementation is not
- **Async codegen** is not complete in every path; some branches still emit synchronous code
- **WebSocket auto-reconnect** with backoff and ping/pong keepalive is not yet shipped
- **Qdrant connection pooling** is not yet shipped (retry is)
- **`kern-pkg` is still Python**, not Kern. HTTP GET and tar.gz both work in Kern's stdlib so the port is unblocked, but it is not done
- **Stichting Kern Foundation** is in formation, not yet legally incorporated
- **v0.9.x stability soak** — six months of zero breaking changes before the v1.0 tag is cut

Every item above is on the [ROADMAP](https://codeberg.org/PUAGroup/kern-lang/src/branch/main/ROADMAP.md) with a checkbox. None are aspirational; all are blockers.

## By the numbers

- **871 tests** at 100% pass — 690 conformance, 181 negative
- **83 stdlib modules** across `std`, `data`, `net`, `db`, `sys`
- **23,300 lines** of Kern in the self-hosted compiler
- **27,400 lines** in the C runtime, \~300 FFI bridge functions
- **15 LSP features** via pygls
- **5 fuzz targets** — JSON, HTTP, string, BLAKE2b, base64 — libFuzzer + ASAN/UBSAN
- **Reproducible builds** verified byte-for-byte identical with `SOURCE_DATE_EPOCH`
- **EUPL-1.2**, hosted on Codeberg, governed from the Netherlands

## Try v0.6.0

Install on Linux or macOS. Single binary, no runtime dependencies.

`curl -fsSL https://kern-lang.eu/install.sh | bash`

[View on Codeberg ](https://codeberg.org/PUAGroup/kern-lang)
