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 auto
shape_registry(). - Under-fetch (#173, ERROR). A block reads
shapevar.field(single-object access) wherefieldis neither a SELECT column nor a declaredcomputedmember 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 via
override_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 through
validate() -- 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, theonjoin column present as a child field, the parentkeypresent). 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_injectableso 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 intodepends_on; - block-local bindings (
{% set %}/let/export/capture/def/region/ loop targets / def params), collected from the block body; - the literal context keys
errorandform(reactivedepends_onnoise); - derived accessors on the Shape dataclass -- a
shapevar.nameread wherenameis 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, no
root.field); - macro/def-arg reads (the def name leaks; field reads do not);
- opaque shapes;
- templates under
chirp//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
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 /…
Normalize indentation soast.parseaccepts a nested handler source.
Return the_ShapeMeta sidecar on a @shape class, else None.
Return the_CompositeMeta sidecar on a @composite class, else None.
Duck-typed (a frozen sidecar exposingmembers) so this rule stays importable for…
Resolve a composite field name to its member@shape class, else None.
A block bound topage.field (where page is a @composite)…
Return the dataclass field names of a@shapeclass (empty if not one).
Return the Shape's derived accessors -- real attributes, not dataclass fields.
A@shape-decorated dataclass commonly exposes a @propertyor method over its columns…
Resolve a binding value to a@shapeclass.
Accepts either the class itself or a registry name string. Returns the class when it carries…
Return the value of a string-literal AST node, elseNone.
ResolveShape.fetch(SomeShape, ...) to the SomeShapeclass.
Matchesfetch / fetch_one / stream on a receiver named Shapewhose first positional arg is a…
ResolveComposite.load(SomePage, ...) to the SomePageclass.
Matchesload on a receiver named Compositewhose first positional arg
is a bareNameresolvable in…
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…
Map handler-local names to the@compositeclass they were loaded from.
Recognizespage = await Composite.load(SomePage, ...)(and the non-await
form). Only single-targetName…
Resolve a render-context kwarg value to a bound Shape class.
Statically-decidable forms:
shapevar=some_localwheresome_local = await Shape.fetch(S, ...);shapevar=Shape.fetch(S, ...)(inline fetch…
Recover(template, block, shapevar, shape_cls)from handler return calls.
AST-walks the handler forFragment/Page/Suspensecalls. The first positional is the…
Return the kida ASTBlock node for block_name, or None.
Depth-first walk of a kida AST yielding every child node.
Yield child nodes from the kida-AST container attributes.
Names bound inside a block (subtract before any field claim).
Walks the block body collecting{% set %} / let / export/
capture…
Names bound by a kidaFortarget expression (incl. tuple targets).
Parameter names declared on a kidaDef/Macro/Regionnode.
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…
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…
Return a fresh OWN-readsDependencyWalker subclass instance, or None.
The walker reuses kida's ownDependencyWalker(the single source of truth for what counts…
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…
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…
Map the bound block + each nested descendant block to its nesting depth.
The bound block is depth 0; a block nested one level…
Return the innermost BOUND block whose OWN reads containread_path.
Attribution must name a block whose Shape contract was actually verified -- i.e.…
Returnshapevar.field paths in a block guarded by | default(none).
Used for #11: the under-fetch remediation hint only tells the author to delete…
Extract theshapevar.fieldreads of a single binding.
Returns(read_fields, read_accessor): the set of first-attr field names
read offshapevar(after subtracting noise…
Flag surface-contract names whose target resolves to no registered Shape.
surface_contractsmaps a surface name -> the expected Shape name. A
target absent from …
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…
Best-effort registry/class name for a Shape (for de-dup and messages).
Runvalidate() over every USED Shape (#169/#167).
Shape.validateis the runtime fail-loud authority: for a scoped Shape it asserts the compiler OUTPUT carries…
Verify@shaperender bindings and surface-contract registry drift.
See the module docstring for the full claim set, ownership boundary, escape hatches, and conservatism contract.…
_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_localwheresome_local = await Shape.fetch(S, ...);shapevar=Shape.fetch(S, ...)(inline fetch as the kwarg value);shapevar=SwhereSis a@shapeclass in handler globals;shapevar=page.fieldwherepage = await Composite.load(P, ...)andfieldis a member Shape of the compositeP(#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