Module

contracts

Typed hypermedia contracts — compile-time validation of the server-client surface.

Validates that the hypermedia surface is internally consistent:

everyhx-get, hx-post, actionattribute in templates resolves to a registered route, and every Fragment/SSE return type references a valid template and block.

This gives Chirp something React/Next.js doesn't have: compile-time validation of the full server-client boundary without JavaScript.

Usage::

# In development, validate on startup:
app._ensure_frozen()
issues = check_hypermedia_surface(app)
for issue in issues:
    print(f"{issue.severity}: {issue.message}")

# Or via CLI:
#   chirp check

Classes

Severity 0
Severity of a contract validation issue.

Severity of a contract validation issue.

ContractIssue 6
A single validation issue found during contract checking.

A single validation issue found during contract checking.

Attributes

Name Type Description
severity Severity
category str
message str
template str | None
route str | None
details str | None
FragmentContract 2
Declares that a route returns a specific template fragment. Used for documentation and validation.…

Declares that a route returns a specific template fragment.

Used for documentation and validation. Chirp verifies at freeze time that the template and block exist.

Attributes

Name Type Description
template str
block str
SSEContract 2
Declares the event types an SSE endpoint emits. Used for documentation and validation. Optionally…

Declares the event types an SSE endpoint emits.

Used for documentation and validation. Optionally declares fragments that the SSE stream yields, soapp.check()can verify the templates/blocks exist.

Attributes

Name Type Description
event_types frozenset[str]
fragments tuple[FragmentContract, ...]
FormContract 3
Declares which dataclass a route binds form data to. Used by ``app.check()`` to verify that ``<inp…

Declares which dataclass a route binds form data to.

Used byapp.check() to verify that <input name="...">, <select name="...">, and <textarea name="...">fields in the template match the dataclass fields expected by the handler.

Attributes

Name Type Description
datacls type
template str
block str | None
RouteContract 3
Full contract metadata for a route. Attached to routes via the ``contract`` parameter on ``@app.ro…

Full contract metadata for a route.

Attached to routes via thecontract parameter on @app.route().

Attributes

Name Type Description
returns FragmentContract | SSEContract | None
form FormContract | None
description str
CheckResult 14
Result of a hypermedia surface check.

Result of a hypermedia surface check.

Attributes

Name Type Description
issues list[ContractIssue]
routes_checked int
templates_scanned int
targets_found int
hx_targets_validated int
dead_templates_found int
sse_fragments_validated int
forms_validated int
component_calls_validated int
page_context_warnings int

Methods

errors 0 list[ContractIssue]
property
def errors(self) -> list[ContractIssue]
Returns
list[ContractIssue]
warnings 0 list[ContractIssue]
property
def warnings(self) -> list[ContractIssue]
Returns
list[ContractIssue]
ok 0 bool
property
def ok(self) -> bool
Returns
bool
summary 0 str
Human-readable summary.
def summary(self) -> str
Returns
str

Functions

_extract_targets_from_source 1 list[tuple[str, str]]
Extract (attr_name, url) pairs from template source text. Scans raw template s…
def _extract_targets_from_source(source: str) -> list[tuple[str, str]]

Extract (attr_name, url) pairs from template source text.

Scans raw template source for htmx URL attributes and form actions. This catches static URLs in HTML; dynamic URLs (from template expressions) are not captured and should be validated separately.

Parameters
Name Type Description
source str
Returns
list[tuple[str, str]]
_extract_hx_target_selectors 1 list[str]
Extract ``hx-target`` values from template source. Returns raw selector string…
def _extract_hx_target_selectors(source: str) -> list[str]

Extracthx-targetvalues from template source.

Returns raw selector strings, skipping template expressions.

Parameters
Name Type Description
source str
Returns
list[str]
_extract_static_ids 1 set[str]
Extract all static ``id`` attribute values from template HTML. Skips IDs conta…
def _extract_static_ids(source: str) -> set[str]

Extract all staticidattribute values from template HTML.

Skips IDs containing template expressions (these are dynamic and cannot be validated at compile time).

Parameters
Name Type Description
source str
Returns
set[str]
_extract_template_references 1 set[str]
Extract template names referenced via Jinja tags. Catches static references fr…
def _extract_template_references(source: str) -> set[str]

Extract template names referenced via Jinja tags.

Catches static references from{% extends %}, {% include %}, {% from %}, and {% import %}. Dynamic expressions (e.g.{% include variable %}) are not captured.

Parameters
Name Type Description
source str
Returns
set[str]
_extract_form_field_names 1 set[str]
Extract ``name`` attribute values from form fields in template HTML. Scans for…
def _extract_form_field_names(source: str) -> set[str]

Extractnameattribute values from form fields in template HTML.

Scans for<input>, <select>, and <textarea>elements. Skips template expressions and framework-injected fields like CSRF tokens.

Parameters
Name Type Description
source str
Returns
set[str]
_closest_field 3 str | None
Find the closest field name by edit distance, or ``None``.
def _closest_field(target: str, fields: set[str], *, max_dist: int = 2) -> str | None
Parameters
Name Type Description
target str
fields set[str]
max_dist int Default:2
Returns
str | None
_edit_distance 2 int
Levenshtein distance between two strings.
def _edit_distance(a: str, b: str) -> int
Parameters
Name Type Description
a str
b str
Returns
int
_closest_id 3 str | None
Find the closest ID by edit distance, or ``None`` if nothing is close.
def _closest_id(target: str, ids: set[str], *, max_dist: int = 3) -> str | None
Parameters
Name Type Description
target str
ids set[str]
max_dist int Default:3
Returns
str | None
_check_hx_target_selectors 2 tuple[list[ContractIssue…
Validate ``hx-target`` selectors against the pool of static element IDs. Only …
def _check_hx_target_selectors(template_sources: dict[str, str], all_ids: set[str]) -> tuple[list[ContractIssue], int]

Validatehx-targetselectors against the pool of static element IDs.

Only validates simple#id selectors. Extended selectors (closest, find, next, previous) and special keywords (this, body) are skipped — they can't be validated statically.

Parameters
Name Type Description
template_sources dict[str, str]
all_ids set[str]
Returns
tuple[list[ContractIssue], int]
_check_hx_indicator_selectors 2 list[ContractIssue]
Validate ``hx-indicator`` selectors against the pool of static element IDs. On…
def _check_hx_indicator_selectors(template_sources: dict[str, str], all_ids: set[str]) -> list[ContractIssue]

Validatehx-indicatorselectors against the pool of static element IDs.

Only validates simple#id selectors. Extended selectors (closest, find, inherit) and comma-separated multiples are skipped.

Parameters
Name Type Description
template_sources dict[str, str]
all_ids set[str]
Returns
list[ContractIssue]
_check_hx_boost 1 list[ContractIssue]
Validate ``hx-boost`` values are ``"true"`` or ``"false"``. htmx silently trea…
def _check_hx_boost(template_sources: dict[str, str]) -> list[ContractIssue]

Validatehx-boost values are "true" or "false".

htmx silently treats any non-"true"value as false, which is confusing. Chirp catches this at compile time.

Parameters
Name Type Description
template_sources dict[str, str]
Returns
list[ContractIssue]
_check_sse_self_swap 1 list[ContractIssue]
Error when ``sse-swap`` is on the same element as ``sse-connect``. htmx's SSE …
def _check_sse_self_swap(template_sources: dict[str, str]) -> list[ContractIssue]

Error whensse-swap is on the same element as sse-connect.

htmx's SSE extension usesquerySelectorAll on the sse-connect element to find swap targets.querySelectorAllnever includes the root element itself, sosse-swapon the same element is a silent no-op — events arrive but nothing is swapped.

The fix is to movesse-swapto a child element.

Parameters
Name Type Description
template_sources dict[str, str]
Returns
list[ContractIssue]
_check_sse_connect_scope 2 list[ContractIssue]
Warn when ``sse-connect`` is inside a broad ``hx-target`` scope. If a layout s…
def _check_sse_connect_scope(template_sources: dict[str, str], broad_targets: set[str]) -> list[ContractIssue]

Warn whensse-connect is inside a broad hx-targetscope.

If a layout setshx-target="#app-content"and an SSE container does not usehx-disinheritto break inheritance, every sse-swapdescendant inherits the broad target and SSE fragments replace the wrong region.

The fix ishx-disinherit="hx-target hx-swap"on the sse-connectelement.

Parameters
Name Type Description
template_sources dict[str, str]
broad_targets set[str]
Returns
list[ContractIssue]
_collect_broad_targets 1 set[str]
Collect broad inherited ``hx-target`` values from layout-level elements. Retur…
def _collect_broad_targets(template_sources: dict[str, str]) -> set[str]

Collect broad inheritedhx-targetvalues from layout-level elements.

Returns a set of"#target (template)"strings. Broad targets are those on<body>, <main>, or elements with hx-boost="true"— scopes likely to be inherited by many descendants.

Parameters
Name Type Description
template_sources dict[str, str]
Returns
set[str]
_check_swap_safety 1 list[ContractIssue]
Warn on mutation tags that may inherit broad container targets. This catches a…
def _check_swap_safety(template_sources: dict[str, str]) -> list[ContractIssue]

Warn on mutation tags that may inherit broad container targets.

This catches a common htmx footgun:

  • a layout-level element sets a broadhx-target="#app-content"
  • a mutating request does not set its own target
  • a fragment response can replace the whole container accidentally.
Parameters
Name Type Description
template_sources dict[str, str]
Returns
list[ContractIssue]
_normalize_sse_url 1 str
Normalize a ``sse-connect`` URL for route matching. Jinja template expressions…
def _normalize_sse_url(url: str) -> str

Normalize asse-connectURL for route matching.

Jinja template expressions like{{ doc.id }}are replaced with __p__ so that _path_matches_routetreats them as concrete segments that match{param}route patterns.

Parameters
Name Type Description
url str
Returns
str
_extract_sse_swap_values 1 set[str]
Extract all ``sse-swap`` event names from a template source.
def _extract_sse_swap_values(source: str) -> set[str]
Parameters
Name Type Description
source str
Returns
set[str]
_check_sse_event_crossref 2 list[ContractIssue]
Cross-reference ``sse-swap`` values against ``SSEContract.event_types``. For e…
def _check_sse_event_crossref(template_sources: dict[str, str], router: Router) -> list[ContractIssue]

Cross-referencesse-swap values against SSEContract.event_types.

For each template withsse-connect, match the URL to a route. If the route declaresSSEContract(event_types=...), verify that:

  • Everysse-swap value in the template exists in event_types (template listens for events the stream never emits).
  • Everyevent_types entry has a matching sse-swapin the template (stream emits events no template listens for).
Parameters
Name Type Description
template_sources dict[str, str]
router Router
Returns
list[ContractIssue]
_check_accessibility 2 list[ContractIssue]
Check for htmx URL attributes on non-interactive elements. Warns when ``hx-get…
def _check_accessibility(source: str, template_name: str) -> list[ContractIssue]

Check for htmx URL attributes on non-interactive elements.

Warns whenhx-get, hx-post, etc. appear on elements like <div> or <span> without role or tabindexattributes that would make them accessible to keyboard and screen reader users.

Parameters
Name Type Description
source str
template_name str
Returns
list[ContractIssue]
_attr_to_method 1 str
Map an htmx attribute name to its HTTP method.
def _attr_to_method(attr: str) -> str
Parameters
Name Type Description
attr str
Returns
str
_collect_route_paths 1 dict[str, frozenset[str]]
Build a mapping of path → allowed methods from the router. Returns a dict wher…
def _collect_route_paths(router: Router) -> dict[str, frozenset[str]]

Build a mapping of path → allowed methods from the router.

Returns a dict where keys are route path patterns and values are sets of allowed HTTP methods. Multiple routes on the same path have their methods merged.

Parameters
Name Type Description
router Router
Returns
dict[str, frozenset[str]]
_path_matches_route 2 bool
Check if a URL could match a route pattern. Handles static paths exactly and p…
def _path_matches_route(url: str, route_path: str) -> bool

Check if a URL could match a route pattern.

Handles static paths exactly and parameterized paths approximately (any path segment can match a{param}segment).

Parameters
Name Type Description
url str
route_path str
Returns
bool
check_hypermedia_surface 1 CheckResult
Validate the hypermedia surface of a Chirp application. **Checks:** 1. **Fragm…
def check_hypermedia_surface(app: App) -> CheckResult

Validate the hypermedia surface of a Chirp application.

Checks:

  1. Fragment references: Every route with a FragmentContract references a template and block that exist.
  2. SSE fragment references: SSEContract fragments resolve to valid templates and blocks.
  3. htmx URL targets: Everyhx-get, hx-post, etc. in template HTML resolves to a registered route with the correct method.
  4. hx-target selectors: Everyhx-target="#id"in template HTML references an element ID that exists somewhere in the template tree (warning — IDs may come from dynamic expressions or JS).
  5. Accessibility: htmx URL attributes on non-interactive elements (<div>, <span>, etc.) without role or tabindex (warning, not error).
  6. Form field validation: Routes with FormContract have template fields that match the dataclass fields.
  7. Orphan routes: Routes that are never referenced from templates (info, not error).
  8. Dead templates: Templates not referenced by any route or other template (info, not error).
  9. Page context gaps: Routes with FragmentContract where the full template requires variables the target block does not — a sign that full-page Page renders may crash at runtime (warning).
  10. Component call validation:{% call %}sites match {% def %}signatures (requires kida typed def support).
Parameters
Name Type Description
app App

A frozen Chirp application.

Returns
CheckResult
_check_inline_templates 2 None
Warn about routes whose return annotation includes InlineTemplate. InlineTempl…
def _check_inline_templates(router: Router, result: CheckResult) -> None

Warn about routes whose return annotation includes InlineTemplate.

InlineTemplate is a prototyping shortcut and should be replaced with file-based templates before shipping to production.

Parameters
Name Type Description
router Router
result CheckResult
_load_template_sources 1 dict[str, str]
Load all template sources from the kida environment's loader. Returns a dict o…
def _load_template_sources(kida_env: Any) -> dict[str, str]

Load all template sources from the kida environment's loader.

Returns a dict of template_name → source_text.

Parameters
Name Type Description
kida_env Any
Returns
dict[str, str]
contract 3 Any
Attach a contract to a route handler. Usage:: @app.route("/search", metho…
def contract(returns: FragmentContract | SSEContract | None = None, *, form: FormContract | None = None, description: str = '') -> Any

Attach a contract to a route handler.

Usage::

@app.route("/search", methods=["POST"])
@contract(returns=FragmentContract("search.html", "results"))
async def search(request):
    ...
Parameters
Name Type Description
returns FragmentContract | SSEContract | None Default:None
form FormContract | None Default:None
description str Default:''
Returns
Any