contracts.rules_shapecheck

Page actions AI-ready formats and sharing
Open LLM text
Share with AI
Ask Claude Ask ChatGPT Ask Gemini Ask Copilot

Verified-Shape render contract (shapecheck).

Thechirp.contracts.rules_data_shapes datacontract verifies the query side: a SELECTed column that maps to no field on thedb.fetch(cls, sql) dataclass is drift. shapecheckverifies the …

Verified-Shape render contract (shapecheck).

Thechirp.contracts.rules_data_shapes datacontract verifies the query side: a SELECTed column that maps to no field on thedb.fetch(cls, sql) dataclass is drift. shapecheckverifies the render side of a @shape-decorated row model (chirp.data.shapes): the fields a template block reads must be fields the bound Shape actually fetched (SELECT columns) or declared (computed=), and a surface-contract registry name must resolve to a real registered Shape.

It owns four statically-decidable claims:

  • Registry drift (#166, ERROR — the marquee). A surface-contract name whose target resolves to no registered Shape is a typo / a renamed-away view. This is fully static (registry-name -> backing-class resolution), zero false-positive, and the highest-value check. It runs even with no contract data registered, via the autoshape_registry().
  • Under-fetch (#173, ERROR). A block readsshapevar.field(single-object access) wherefield is neither a SELECT column nor a declared computed member of the bound Shape: the value silently renders asNoneat runtime.
  • Over-fetch (#166, WARNING — default, promotable). A Shape column no bound block reads. Block coverage is incomplete static information, so this is humble by default and can be promoted viaoverride_contract_severity.
  • Un-injectable tenant scope (#169, ERROR — §8.1). A Shape declares scope=but its declared SQL is opaque/un-injectable (CTE / UNION / SELECT */ no analyzable FROM), so the bounded compiler cannot structurally inject the scope predicate and the query would silently leak across tenants. The tenant-scope guarantee is delivered by structural injection on the compiler output, never by a flaky WHERE-column scanner; this is the single statically-decidable scope ERROR, reported with an actionable message by _check_scope_injectable().
  • Un-compilable Shape (#167/#169, ERROR). Every Shape the app actually uses is run throughvalidate() -- the runtime fail-loud authority. It asserts the compiler output carries the scope predicate for a scoped Shape, and that everynested()child is batchable (analyzable FROM, theon join column present as a child field, the parent keypresent). AShapeErrorbecomes a startup ERROR so a Shape that would crash or silently misbehave at runtime failsapp.check()instead. The scope-injectability case is de-duplicated against_check_scope_injectable so it fires exactly one ERROR.

Ownership boundary (no double-fire withdata): datamatches only db.fetch(cls, sql) db-handle receivers; Shape.fetch(...)has the Shape class as its receiver, which _is_db_receiverrejects. The two categories cannot fire on the same call site.

Conservatism contract (skip-not-guess — this is a fail-loud ERROR category, so a false positive breaks freeze in debug): the whole body is wrapped so any unexpected analysis error returns[] and never crashes app.check(). The field-level claim is made only for single-objectshapevar.fieldaccess; the dominant list/table ({% for c in rows %}) and macro-arg patterns collapse to the collection root independs_onand are invisible -- shapecheck verifies the root is bound, not the per-item fields. Opaque shapes (SELECT */ expressions / CTE / UNION ->columns == ()) are an explicit escape hatch.

Escape hatches subtracted from a block's reads before any field claim:

  • template globals (url_for, csrf_token, csp_nonce, _, range, len ...) -- they leak into depends_on;
  • block-local bindings ({% set %} / let / export / capture/ def / region/ loop targets / def params), collected from the block body;
  • the literal context keyserror and form (reactive depends_onnoise);
  • derived accessors on the Shape dataclass -- ashapevar.nameread where name is a real class-level attribute (a @property, method, or descriptor) on the bound dataclass but not a dataclass field. These are idiomatic derived members (the reason to use a dataclass over a tuple); they resolve at runtime and render correctly, and the columns they consume live inside the accessor body wheredepends_oncannot see them -- so a derived-accessor read also suppresses the per-binding over-fetch claim (its column coverage is invisibly incomplete);
  • loop-collapsed reads (only the collection root appears, noroot.field);
  • macro/def-arg reads (the def name leaks; field reads do not);
  • opaque shapes;
  • templates underchirp/ / chirpui/.

Default severities (ship the proper design): registry-drift = ERROR, under-fetch = ERROR, over-fetch = WARNING. All overridable via app.override_contract_severity("shapecheck", ...).

contracts.rules_shapecheck

Name Type Default Description
type
qualified_name
element_type
description
source_file
line_number
is_autodoc
autodoc_element
_autodoc_template
_autodoc_url_path
_autodoc_page_type
title
doc_content_hash

Symbols on this page

class _ShapecheckBinding

Per-binding derived state cached by pass 1 for the pass-2 report.

Pass 1 resolves each(template, block, shapevar, Shape)binding to its columns /…

Jump to symbol
function _dedent

Normalize indentation soast.parseaccepts a nested handler source.

Jump to symbol
function _shape_meta

Return the_ShapeMeta sidecar on a @shape class, else None.

Jump to symbol
function _composite_meta

Return the_CompositeMeta sidecar on a @composite class, else None.

Duck-typed (a frozen sidecar exposingmembers) so this rule stays importable for…

Jump to symbol
function _composite_field_shape

Resolve a composite field name to its member@shape class, else None.

A block bound topage.field (where page is a @composite)…

Jump to symbol
function _shape_field_names

Return the dataclass field names of a@shapeclass (empty if not one).

Jump to symbol
function _derived_accessors

Return the Shape's derived accessors -- real attributes, not dataclass fields.

A@shape-decorated dataclass commonly exposes a @propertyor method over its columns…

Jump to symbol
function _resolve_shape

Resolve a binding value to a@shapeclass.

Accepts either the class itself or a registry name string. Returns the class when it carries…

Jump to symbol
function _string_arg

Return the value of a string-literal AST node, elseNone.

Jump to symbol
function _shape_from_call

ResolveShape.fetch(SomeShape, ...) to the SomeShapeclass.

Matchesfetch / fetch_one / stream on a receiver named Shapewhose first positional arg is a…

Jump to symbol
function _composite_from_call

ResolveComposite.load(SomePage, ...) to the SomePageclass.

Matchesload on a receiver named Compositewhose first positional arg is a bareNameresolvable in…

Jump to symbol
function _local_shape_bindings

Map handler-local names to the Shape class they were assigned from.

Recognizesname = await Shape.fetch(SomeShape, ...)and name = Shape.fetch(SomeShape, ...) (and fetch_one…

Jump to symbol
function _local_composite_bindings

Map handler-local names to the@compositeclass they were loaded from.

Recognizespage = await Composite.load(SomePage, ...)(and the non-await form). Only single-targetName…

Jump to symbol
function _kwarg_shape

Resolve a render-context kwarg value to a bound Shape class.

Statically-decidable forms:

  • shapevar=some_local where some_local = await Shape.fetch(S, ...);
  • shapevar=Shape.fetch(S, ...)(inline fetch…
Jump to symbol
function _static_bindings

Recover(template, block, shapevar, shape_cls)from handler return calls.

AST-walks the handler forFragment/Page/Suspensecalls. The first positional is the…

Jump to symbol
function _block_node

Return the kida ASTBlock node for block_name, or None.

Jump to symbol
function _walk_kida

Depth-first walk of a kida AST yielding every child node.

Jump to symbol
function _kida_children

Yield child nodes from the kida-AST container attributes.

Jump to symbol
function _block_local_names

Names bound inside a block (subtract before any field claim).

Walks the block body collecting{% set %} / let / export/ capture…

Jump to symbol
function _for_target_names

Names bound by a kidaFortarget expression (incl. tuple targets).

Jump to symbol
function _param_names

Parameter names declared on a kidaDef/Macro/Regionnode.

Jump to symbol
function _iter_kida_descendants

Depth-first walk of a kida AST via the nativeiter_child_nodes().

Yields every descendant node (excludingnodeitself). Uses kida's own iter_child_nodes() (dataclass-field…

Jump to symbol
function _nested_block_nodes

Return theBlock descendants nested inside block_node.

A parent block'sdepends_onis kida's documented conservative SUPERSET: it absorbs every nested child block's reads…

Jump to symbol
function _own_reads_walker

Return a fresh OWN-readsDependencyWalker subclass instance, or None.

The walker reuses kida's ownDependencyWalker(the single source of truth for what counts…

Jump to symbol
function _own_block_reads

Returnblock_name's OWN reads, pruned of nested-child bleed.

kida'sdepends_onfor a parent block is a conservative SUPERSET that absorbs every nested child…

Jump to symbol
function _own_reads_by_block

Map each ofblock_namesto its OWN (bleed-pruned) reads.

Used for under-fetch attribution: a flagged read path is reported against the innermost block whose…

Jump to symbol
function _block_depths

Map the bound block + each nested descendant block to its nesting depth.

The bound block is depth 0; a block nested one level…

Jump to symbol
function _innermost_owner

Return the innermost BOUND block whose OWN reads containread_path.

Attribution must name a block whose Shape contract was actually verified -- i.e.…

Jump to symbol
function _default_none_guarded_reads

Returnshapevar.field paths in a block guarded by | default(none).

Used for #11: the under-fetch remediation hint only tells the author to delete…

Jump to symbol
function _binding_read_fields

Extract theshapevar.fieldreads of a single binding.

Returns(read_fields, read_accessor): the set of first-attr field names read offshapevar(after subtracting noise…

Jump to symbol
function _check_registry_drift

Flag surface-contract names whose target resolves to no registered Shape.

surface_contractsmaps a surface name -> the expected Shape name. A target absent from …

Jump to symbol
function _check_scope_injectable

Flag scoped Shapes whose SQL is opaque/un-injectable (#169, §8.1 #3).

The tenant-scope guarantee is delivered by structural injection on the compiler output -- not…

Jump to symbol
function _shape_name

Best-effort registry/class name for a Shape (for de-dup and messages).

Jump to symbol
function _check_shape_validate

Runvalidate() over every USED Shape (#169/#167).

Shape.validateis the runtime fail-loud authority: for a scoped Shape it asserts the compiler OUTPUT carries…

Jump to symbol
function check_shapecheck

Verify@shaperender bindings and surface-contract registry drift.

See the module docstring for the full claim set, ownership boundary, escape hatches, and conservatism contract.…

Jump to symbol
_ShapecheckBinding
class

Per-binding derived state cached by pass 1 for the pass-2 report.

Pass 1 resolves each(template, block, shapevar, Shape)binding to its columns / computed / provided fields, occurrence-granular own reads, and attribution metadata exactly once; pass 2 emits under-fetch (per binding) and over-fetch (against the shape-group read union) from these records.

_dedent
function
def _dedent(src: str) -> str

Normalize indentation soast.parseaccepts a nested handler source.

Parameters

Name Type Default Description
src str
_shape_meta
function
def _shape_meta(cls: Any) -> Any | None

Return the_ShapeMeta sidecar on a @shape class, else None.

Parameters

Name Type Default Description
cls Any
_composite_meta
function
def _composite_meta(cls: Any) -> Any | None

Return the_CompositeMeta sidecar on a @composite class, else None.

Duck-typed (a frozen sidecar exposingmembers) so this rule stays importable for db-less / data-less apps, mirroring_shape_meta().

Parameters

Name Type Default Description
cls Any
_composite_field_shape
function
def _composite_field_shape(composite_cls: Any, field: str) -> Any | None

Resolve a composite field name to its member@shape class, else None.

A block bound topage.field (where page is a @composite) reads the fields of that field's member Shape; the per-block subset check then runs against the composite member's provided fields -- never one query per block (§4-L4). ReturnsNonewhen the field is not a Shape member (skip).

Parameters

Name Type Default Description
composite_cls Any
field str
_shape_field_names
function
def _shape_field_names(cls: Any) -> frozenset[str]

Return the dataclass field names of a@shapeclass (empty if not one).

Parameters

Name Type Default Description
cls Any
_derived_accessors
function
def _derived_accessors(cls: Any) -> frozenset[str]

Return the Shape's derived accessors -- real attributes, not dataclass fields.

A@shape-decorated dataclass commonly exposes a @propertyor method over its columns (full_name over first_name/last_name, url() over slug). Such a name is a real class-level attribute (a property, function, descriptor, or class var) but NOT a dataclass field -- readingshapevar.full_nameresolves at runtime and renders correctly, so it must never be flagged as an under-fetch. We collect every public class-level attribute that is not a dataclass field and not a dunder; the columns these accessors consume live inside their bodies, invisible to depends_on.

Usesinspect.getattr_static so a propertyis observed as the descriptor object (never triggering its getter against the bare class).

Parameters

Name Type Default Description
cls Any
_resolve_shape
function
def _resolve_shape(value: Any, registry: Mapping[str, type]) -> Any | None

Resolve a binding value to a@shapeclass.

Accepts either the class itself or a registry name string. Returns the class when it carries a_ShapeMeta sidecar, else None(skip).

Parameters

Name Type Default Description
value Any
registry Mapping[str, type]
_string_arg
function
def _string_arg(node: ast.expr | None) -> str | None

Return the value of a string-literal AST node, elseNone.

Parameters

Name Type Default Description
node ast.expr | None
_shape_from_call
function
def _shape_from_call(node: ast.Call, handler_globals: dict[str, Any]) -> Any | None

ResolveShape.fetch(SomeShape, ...) to the SomeShapeclass.

Matchesfetch / fetch_one / stream on a receiver named Shape whose first positional arg is a bareNameresolvable in handler globals. Anything else returnsNone(skip -- not statically a Shape fetch).

Parameters

Name Type Default Description
node ast.Call
handler_globals dict[str, Any]
_composite_from_call
function
def _composite_from_call(node: ast.Call, handler_globals: dict[str, Any]) -> Any | None

ResolveComposite.load(SomePage, ...) to the SomePageclass.

Matchesload on a receiver named Compositewhose first positional arg is a bareName resolvable in handler globals to a @compositeclass. Anything else returnsNone(skip -- not statically a Composite load).

Parameters

Name Type Default Description
node ast.Call
handler_globals dict[str, Any]
_local_shape_bindings
function
def _local_shape_bindings(tree: ast.AST, handler_globals: dict[str, Any]) -> dict[str, Any]

Map handler-local names to the Shape class they were assigned from.

Recognizesname = await Shape.fetch(SomeShape, ...)and name = Shape.fetch(SomeShape, ...) (and fetch_one / stream). Only single-targetNameassignments are tracked. The resolved value is the Shape class (from handler globals) --Noneentries are dropped.

Parameters

Name Type Default Description
tree ast.AST
handler_globals dict[str, Any]
_local_composite_bindings
function
def _local_composite_bindings(tree: ast.AST, handler_globals: dict[str, Any]) -> dict[str, Any]

Map handler-local names to the@compositeclass they were loaded from.

Recognizespage = await Composite.load(SomePage, ...)(and the non-await form). Only single-targetNameassignments are tracked. The resolved value is the@composite class from handler globals; Nonedropped.

Parameters

Name Type Default Description
tree ast.AST
handler_globals dict[str, Any]
_kwarg_shape
function
def _kwarg_shape(kw_value: ast.expr, local_shapes: dict[str, Any], local_composites: dict[str, Any], handler_globals: dict[str, Any]) -> Any | None

Resolve a render-context kwarg value to a bound Shape class.

Statically-decidable forms:

  • shapevar=some_local where some_local = await Shape.fetch(S, ...);
  • shapevar=Shape.fetch(S, ...)(inline fetch as the kwarg value);
  • shapevar=S where S is a @shapeclass in handler globals;
  • shapevar=page.field where page = await Composite.load(P, ...)and field is a member Shape of the composite P(#170) -- the block's provided fields come from that composite member's Shape, never one query per block.

Parameters

Name Type Default Description
kw_value ast.expr
local_shapes dict[str, Any]
local_composites dict[str, Any]
handler_globals dict[str, Any]
_static_bindings
function
def _static_bindings(tree: ast.AST, handler_globals: dict[str, Any]) -> list[tuple[str | None, str, str, Any]]

Recover(template, block, shapevar, shape_cls)from handler return calls.

AST-walks the handler forFragment/Page/Suspensecalls. The first positional is the template, the second positional is the block (both must be string literals). Each kwarg whose value resolves to a Shape contributes one binding:shapevar is the kwarg name (the template variable), shape_cls is the resolved class. A kwarg valuepage.field (where pageis a Composite.load(...)local) resolves to that composite field's member Shape -- the per-block subset check runs against the composite member's provided fields (#170).

Parameters

Name Type Default Description
tree ast.AST
handler_globals dict[str, Any]
_block_node
function
def _block_node(env: Any, template_name: str, block_name: str) -> Any | None

Return the kida ASTBlock node for block_name, or None.

Parameters

Name Type Default Description
env Any
template_name str
block_name str
_walk_kida
function
def _walk_kida(node: Any) -> Any

Depth-first walk of a kida AST yielding every child node.

Parameters

Name Type Default Description
node Any
_kida_children
function
def _kida_children(node: Any) -> list[Any]

Yield child nodes from the kida-AST container attributes.

Parameters

Name Type Default Description
node Any
_block_local_names
function
def _block_local_names(block_node: Any) -> frozenset[str]

Names bound inside a block (subtract before any field claim).

Walks the block body collecting{% set %} / let / export/ capture / def / regiontargets, loop variables, and def params via_binding_names() (the canonical single-node collector). Per §8.5 #3 we walk the body rather than calling_binding_nameson the block.

Parameters

Name Type Default Description
block_node Any
_for_target_names
function
def _for_target_names(expr: Any) -> set[str]

Names bound by a kidaFortarget expression (incl. tuple targets).

Parameters

Name Type Default Description
expr Any
_param_names
function
def _param_names(node: Any) -> set[str]

Parameter names declared on a kidaDef/Macro/Regionnode.

Parameters

Name Type Default Description
node Any
_iter_kida_descendants
function
def _iter_kida_descendants(node: Any) -> Any

Depth-first walk of a kida AST via the nativeiter_child_nodes().

Yields every descendant node (excludingnodeitself). Uses kida's own iter_child_nodes() (dataclass-field introspection) rather than this module's hand-rolled_kida_childrenso the traversal covers every container shape kida defines (withtargets, matchcases, etc.) with zero parallel-maintenance hazard.

Parameters

Name Type Default Description
node Any
_nested_block_nodes
function
def _nested_block_nodes(block_node: Any) -> list[Any]

Return theBlock descendants nested inside block_node.

A parent block'sdepends_onis kida's documented conservative SUPERSET: it absorbs every nested child block's reads (verified empirically -- a child block nested under a{% for %}/{% if %}still bleeds into the parent viaDependencyWalker). Used for under-fetch ATTRIBUTION: a flagged path is named against the innermost block whose OWN reads contain it, so we need each nested child's name to analyze it independently.

Parameters

Name Type Default Description
block_node Any
_own_reads_walker
function
def _own_reads_walker() -> Any | None

Return a fresh OWN-readsDependencyWalker subclass instance, or None.

The walker reuses kida's ownDependencyWalker (the single source of truth for what counts as a context read -- scope handling for loop vars /{% set %}/ def params, builtin elision, dotted path building) but overridesvisit_Blockso that nested (non-root) Block nodes are treated as OPAQUE: the root block's body is recursed, but the walker does NOT descend into nested child blocks. The result is the block's OCCURRENCE-granular own reads -- a dotted read that occurs in BOTH the parent body and a nested child is retained (it genuinely lives in the parent), and a read that occurs ONLY inside a nested child is excluded (no parent bleed).

ReturnsNonewhen kida's dependency machinery is unavailable (skip-not- guess -- this is a fail-loud ERROR category, so an analysis we cannot perform must drop to no claim, never a guess).

No parameters.

_own_block_reads
function
def _own_block_reads(env: Any, template_name: str, block_name: str) -> frozenset[str] | None

Returnblock_name's OWN reads, pruned of nested-child bleed.

kida'sdepends_onfor a parent block is a conservative SUPERSET that absorbs every nested child block's reads. Naiveown = depends_on(parent) - union(depends_on(child))(set difference of analysis sets) is a FALSE-NEGATIVE hazard: kida'sdepends_onis set-granularity, so when the SAME dotted read (e.g.board.owner) occurs in BOTH the parent body AND a nested child under the same shapevar, subtraction removes it from the parent ENTIRELY -- a genuine parent under-fetch is silently MISSED.

We instead compute own reads at OCCURRENCE granularity by reusing kida's own DependencyWalker with visit_Block overridden to treat nested child Blocks as opaque (see _own_reads_walker()). A read in both the parent body and a nested child is retained on the parent; a read ONLY in a nested child is excluded. This fires the marquee parent under-fetch without re-introducing nested-child bleed.

ReturnsNonewhen the template/block/walker cannot be analyzed (skip). kida's reads are treated strictly as a conservative superset -- this rule never assumes kida prunes.

Parameters

Name Type Default Description
env Any
template_name str
block_name str
_own_reads_by_block
function
def _own_reads_by_block(env: Any, template_name: str, block_names: frozenset[str]) -> dict[str, frozenset[str]]

Map each ofblock_namesto its OWN (bleed-pruned) reads.

Used for under-fetch attribution: a flagged read path is reported against the innermost block whose OWN reads contain it, not the bound ancestor block. Blocks that cannot be analyzed are simply absent from the map.

Parameters

Name Type Default Description
env Any
template_name str
block_names frozenset[str]
_block_depths
function
def _block_depths(env: Any, template_name: str, block_name: str) -> dict[str, int]

Map the bound block + each nested descendant block to its nesting depth.

The bound block is depth 0; a block nested one level deeper is depth 1, etc. Used by_innermost_owner() to break attribution ties toward the syntactically-innermost block. Returns{}when the block cannot be found.

Parameters

Name Type Default Description
env Any
template_name str
block_name str
_innermost_owner
function
def _innermost_owner(read_path: str, own_reads_by_block: dict[str, frozenset[str]], block_depths: dict[str, int], fallback: str, bound_blocks: frozenset[str]) -> str

Return the innermost BOUND block whose OWN reads containread_path.

Attribution must name a block whose Shape contract was actually verified -- i.e. a block inbound_blocks(the set of blocks this app binds to a Shape). The innermost owner of a flagged read may be an UNBOUND nested child (e.g. a{% block badge %} nested inside a bound {% block header %}that shares the read): naming that sibling/child points the developer at a block whose contract was never checked, when the failure belongs to the BOUND binding (header). So we restrict to bound owners and pick the innermost of those; the read still fires (no false negative) -- only the reported block name changes to a verified binding.

Ties (same depth) are unlikely (a given read lives in one block); when no bound block claims the path -- e.g. a duplicate-name AST/metadata shadow made the bound block's own-reads diverge -- attribute tofallback(the bound block being checked) so a real under-fetch is never silently dropped.

Parameters

Name Type Default Description
read_path str
own_reads_by_block dict[str, frozenset[str]]
block_depths dict[str, int]
fallback str
bound_blocks frozenset[str]
_default_none_guarded_reads
function
def _default_none_guarded_reads(block_node: Any) -> frozenset[str]

Returnshapevar.field paths in a block guarded by | default(none).

Used for #11: the under-fetch remediation hint only tells the author to delete the| default(none)guard when the flagged read actually carries one. A guard appears in the kida AST as aFilter(name='default')wrapping aGetattr(obj=Name, attr=...) whose sole positional arg is the None constant. We record the dottedobj.attrpath for each such filter.

Parameters

Name Type Default Description
block_node Any
_binding_read_fields
function
def _binding_read_fields(reads: frozenset[str], shapevar: str, accessors: frozenset[str], env_globals: frozenset[str], local_names: frozenset[str]) -> tuple[set[str], bool]

Extract theshapevar.fieldreads of a single binding.

Returns(read_fields, read_accessor): the set of first-attr field names read offshapevar(after subtracting noise / globals / block-locals and derived accessors), and whether any read resolved to a derived accessor (a @property/ method / descriptor). A derived-accessor read makes the binding's column coverage invisibly incomplete -- the over-fetch claim is suppressed for the whole shape group when any binding read one.

Shared by the over-fetch group-union pre-pass and the per-binding under-fetch pass so both observe identical read-field semantics (no parallel drift).

Parameters

Name Type Default Description
reads frozenset[str]
shapevar str
accessors frozenset[str]
env_globals frozenset[str]
local_names frozenset[str]
_check_registry_drift
function
def _check_registry_drift(surface_contracts: Mapping[str, Any], registry: Mapping[str, type]) -> list[ContractIssue]

Flag surface-contract names whose target resolves to no registered Shape.

surface_contractsmaps a surface name -> the expected Shape name. A target absent fromshape_registry() is drift (a typo or a renamed-away view). Emits one ERROR per drifted surface, with a get_close_matches() suggestion from the live registry names.

Parameters

Name Type Default Description
surface_contracts Mapping[str, Any]
registry Mapping[str, type]
_check_scope_injectable
function
def _check_scope_injectable(shapes: set[type], flagged_names: set[str] | None = None) -> list[ContractIssue]

Flag scoped Shapes whose SQL is opaque/un-injectable (#169, §8.1 #3).

The tenant-scope guarantee is delivered by structural injection on the compiler output -- not a WHERE scan. The ONE statically-decidable ERROR is reserved for a Shape that declaresscope=but whose declared SQL is opaque/un-injectable (CTE / UNION / SELECT * / no analyzable FROM), so the compiler CANNOT inject the predicate and the query would silently leak across tenants. We delegate the injectability decision to the compiler's own _scope_injectable() (single source of truth, no WHERE-column scanner).

Scoped only overshapes-- the set of Shapes this app actually uses (route-bound + resolved surface-contract targets). Iterating the entire process-wide registry would leak unrelated test/app fixtures into every other app'sapp.check(); an app's scope contract is about the Shapes the app binds, not every Shape ever imported.

flagged_names: when given, every Shape name this check ERRORs on is added to it so the caller can de-dup the_check_shape_validate() pass (whose Shape.validatewould raise the same scope failure with a less actionable message).

Parameters

Name Type Default Description
shapes set[type]
flagged_names set[str] | None None
_shape_name
function
def _shape_name(cls: Any) -> str

Best-effort registry/class name for a Shape (for de-dup and messages).

Parameters

Name Type Default Description
cls Any
_check_shape_validate
function
def _check_shape_validate(shapes: set[type], already_flagged: set[str]) -> list[ContractIssue]

Runvalidate() over every USED Shape (#169/#167).

Shape.validateis the runtime fail-loud authority: for a scoped Shape it asserts the compiler OUTPUT carries the scope predicate, and for nested children it asserts each child is batchable (analyzable FROM, theonjoin column present as a field, the parentkeypresent). A ShapeErroris the decoration-time signal that a Shape cannot be safely compiled -- it would crash or silently misbehave at runtime, so we surface it as a startupapp.check()ERROR.

De-dup: the scope-injectability case is already reported by _check_scope_injectable() with a more actionable message; shapes whose name is inalready_flaggedare skipped here so a scoped-opaque Shape fires exactly one ERROR (not the generic validate echo too). Anything Shape.validaterejects that the scope check did NOT (a malformed nested child) still surfaces here.

Parameters

Name Type Default Description
shapes set[type]
already_flagged set[str]
check_shapecheck
function
def check_shapecheck(snapshot: ContractCheckSnapshot) -> list[ContractIssue]

Verify@shaperender bindings and surface-contract registry drift.

See the module docstring for the full claim set, ownership boundary, escape hatches, and conservatism contract. The entire body is wrapped so any unexpected analysis error returns[]-- a built-in rule must never crash app.check().

Parameters

Name Type Default Description
snapshot ContractCheckSnapshot

View source · /home/runner/work/chirp/chirp/site/../src/chirp/contracts/rules_shapecheck.py:1