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 literalSSEEvent(event="…")andFragment(target="…")yields. Asse-swap="X"that matches neither the declaredevent_typesnor any inferred literal is now ERROR at startup (wasWARNING) — the silent-mismatch class of bug (htmx drops unmatched events) fails loud. - Form action contract — reports
<form action="/path" method="post">targets that lack aFormContractdeclaration. - Layout
HX-Target+ outlet —LayoutChain.find_start_index_for_targetmatches{# outlet: element_id #}as well as{# target: #}, so boostedHX-Target: #mainresolves for chirp-ui app shells. - OOB target contract — warns when
hx-swap-oobelements reference IDs not found in any template. - Reactive contract rules — validates that
DependencyIndexblock 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 contract —
chirp checkno longer reports missing_meta.pyfor routes whose_meta.pydefines onlymeta()(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-op
from __future__ import annotationsimports (PEP 649 default on 3.14), added 11 TypedDicts for stable dict shapes in tools/debug modules, converted 3 dispatch chains tomatch/case.