Chirp 0.5.0

Strict templates by default, fail-loud OOB regions, Alpine on streaming HTML, agent-grounded chirp-ui 0.5, and a deeper contract-check net.

Focus: tighten every silent-failure mode into an explicit error, expand static analysis, and adopt chirp-ui 0.5 + kida 0.7 strict defaults.

This is a pre-1.0 release with three breaking behavior changes. Each has a transitional opt-out; see Migration below.


Breaking

SSE default event name

EventStream now emits yielded Fragment payloads as unnamed SSE frames (htmx's default message channel). Previously the wire frame defaulted to event: fragment, requiring every consumer to add sse-swap="fragment"— a chirp-specific quirk that broke stock htmx-sse snippets.

Migration: changesse-swap="fragment" to sse-swap="message" in templates (htmx-sse needs the attribute present to wire up the listener). The chirp/sse.html macro now defaults to swap="message". To keep the old wire shape, yield SSEEvent(data=rendered, event="fragment") or pass target="fragment" on the Fragment.

AppConfig.strict_undefined = Trueby default

Templates referencing a missing attribute/key raiseUndefinedErrorinstead of silently rendering empty, matching kida 0.7's new default.

Migration: fix callsites with{{ obj.attr ?? "" }}, {{ obj.attr | default("") }}, or {% if obj.attr is defined %}…{% end %}. Transitional opt-out: AppConfig(strict_undefined=False).

Orphan OOB registrations now ERROR at freeze

app.check() emits Severity.ERROR (was WARNING) when the OOB registry contains a block no layout template defines. Non-optional orphans block debug-mode freeze.

Migration: addoptional=True to register_oob_region() for regions that legitimately appear in only some layouts, or add the missing {% region <name> %}…{% end %} to the layout. Global opt-out: app.override_contract_severity("oob_registry", Severity.WARNING).


Highlights

Fail-loud OOB regions

execute_render_plan used to swallow every exception during OOB region render and substitute html = "" — silently wiping DOM content when a block was missing. Missing blocks now raise chirp.errors.BlockNotFoundError (multi-inherits from KeyError for back-compat); render errors propagate to the route error handler. See docs/quality/contracts-debugging/oob-registry.md.

New:register_oob_region(..., optional=True) — opt-out for shell regions that legitimately appear in only some layouts. chirp.ext.chirp_ui's auto-registered breadcrumb, sidebar, title, and shell-actions regions all default to optional=True.

Alpine on streaming HTML

AlpineInject now rewrites StreamingResponse bodies (e.g. Suspense shells) to insert the Alpine bundle before </body>, with the same deduplication as buffered pages. Alpine-powered components light up on streamed pages without manual script tags.

New template global:alpine_json_config("my-id", data) emits a safely-escaped <script type="application/json">tag for wiring Alpine components to server data.

Suspense — explicit deferred blocks

Suspense.defer_blocks — optional explicit list of blocks to re-render as OOB chunks, bypassing block_metadata() static analysis. Use when deferred values are passed through macro arguments the analyzer can't trace. Shell renders now inject __chirp_defer_pending__ (a frozenset of unresolved context keys) so templates can branch on membership instead of overloading None / truthiness. Ancestor blocks whose depends_onis a strict superset of a matched leaf are now pruned, preventing wasted OOB chunks.

Native fragment blocks

Chirp now recognises kida 0.6.0+'s native{% fragment name %}…{% end %} directive as a swap-only target: the block body is suppressed during full-template renders and only emits when addressed by Fragment(...), the /_frag{path} dispatcher, or a {% fragment %} target in a PageShellContract. Use this instead of the {% block %} + {% if foo is defined %} workaround for success panels, SSE payloads, and OOB swap targets. Contract rules updated to treat fragment=Trueblocks as unreachable-by-design.

Expanded contract checks

chirp checkgained four new rules:

  • SSE event cross-reference (AST-based) — walks each@contract(returns=SSEContract(...)) handler for literal SSEEvent(event="…") and Fragment(target="…") yields. A sse-swap="X" that matches neither the declared event_types nor any inferred literal is now ERROR at startup (was WARNING) — the silent-mismatch class of bug (htmx drops unmatched events) fails loud.
  • Form action contract — reports<form action="/path" method="post"> targets that lack a FormContractdeclaration.
  • LayoutHX-Target + outletLayoutChain.find_start_index_for_target matches {# outlet: element_id #} as well as {# target: #}, so boosted HX-Target: #mainresolves for chirp-ui app shells.
  • OOB target contract — warns whenhx-swap-oobelements reference IDs not found in any template.
  • Reactive contract rules — validates thatDependencyIndexblock references point to real template blocks and that derivation graphs are acyclic.

chirp-ui 0.5 + kida 0.7

Bumpschirp-ui>=0.5.0 (agent-grounding manifest, composite contract tests, @layer chirpui.* cascade public API, set_strict("auto") + CHIRP_UI_DEV) and kida-templates>=0.7.0 (strict_undefined=True default, Jinja2 parser hints). use_chirp_ui(app, strict="auto") delegates to the CHIRP_UI_DEV env var so dev hosts opt in once without code changes. Per-request _ChirpUIStrictMiddlewareretired.

Free-threading observability

ReactiveBus gained emitted_count, dropped_count, subscriber_count properties and a configurable maxsize=Nper-subscriber queue (default 256). 25 new stress tests cover every Lock-protected module.


Fixed

  • Route contractchirp check no longer reports missing _meta.py for routes whose _meta.py defines only meta()(dynamic metadata).

Dependencies

  • chirp-ui>=0.5.0 (bumped from >=0.2.5)
  • kida-templates>=0.7.0 (bumped from >=0.3.2)
  • bengal-pounce>=0.6.0 (bumped from >=0.5.1)

Modernization

  • Removed 49 no-opfrom __future__ import annotations imports (PEP 649 default on 3.14), added 11 TypedDicts for stable dict shapes in tools/debug modules, converted 3 dispatch chains to match/case.

Upgrading

uv pip install --upgrade "bengal-chirp>=0.5.0"
pip install --upgrade "bengal-chirp>=0.5.0"