Validation guardrails
Table of contents
Introduction
The framework checks refinement declarations, and the collisions between refiners, every time your app starts. The CLI runs the same rules earlier, so an invalid or colliding refinement never reaches a running product.
splent product:validate --only refinement
4. Refinement (refiners, splent product:validate --only refinement)
✅ No collisions between the refiners of this product (1 refiner)
Where the rules run
product:validate the same facts statically, so both reach the same verdict.| Where | When | On a collision |
|---|---|---|
feature:refine |
While you configure a refiner | A taken target is shown as taken, and a refiner is not offered as a base |
product:validate |
Before boot, and before product:derive and product:build |
Phase 4 fails the product |
| The boot | When the product starts | One FeatureError lists every collision, and the app does not start |
What a declaration must respect
Each refiner may only touch what its base declares extensible in [tool.splent.contract.extensible]. If a rule fails, the app stops with Refinement validation failed: and every error.
| Rule | Error |
|---|---|
| The base is in the product | <refiner>: refines 'shop' but it is not declared in the product. |
| An overridden service, template or hook is extensible | <refiner>: overrides service 'X' from shop, but shop does not declare it as extensible. |
| An extended model is extensible | <refiner>: extends model 'X' from shop, but shop does not declare it as extensible. |
Added routes target a base with routes = true |
<refiner>: adds routes to blueprint 'x_bp' from shop, but shop does not declare routes as extensible. |
Collisions between refiners
Once the declarations are valid, and before any feature loads, the framework compares what every refiner declares and brings.
The refiners of this product collide, so it cannot start.
- splent_feature_collections and splent_feature_sales both add collection_id to the model Item of splent_feature_shop. Rename it in one of the mixins, or declare them incompatible in the UVL with !(collections & sales).
| Code | Collision | Fix |
|---|---|---|
chain |
A refiner refines another refiner | Refine the original base |
duplicate-override |
Two refiners override the same template or service, or the same slot of one base | Keep it in one, or !(a & b) in the UVL |
attribute-twice |
Two refiners add the same attribute, or the same table column, to one model | Rename it in one mixin, or !(a & b) |
shadows-base |
A refiner adds an attribute its base model defines | Rename it, or mark a method with @typing.override |
undeclared-template |
A refiner ships a template its base ships, without declaring it | Declare it, or rename the file |
missing-template |
A refiner declares a template override it does not ship | Add the file, or drop the declaration |
duplicate-route |
An added route repeats a URL for the same method | Give the route another URL |
The same checks run again as each piece is applied, so an undeclared refine_service or replace_template_hook call stops startup with the same wording.
Checking before boot
Phase 4 of product:validate runs every rule without importing any feature. It reads declarations from each pyproject.toml, mixin attributes from the source, and templates from templates/. It also checks the whole SPL, and warns about each pair of colliding refiners the UVL allows together.
⚠️ The UVL model allows collections and sales in one product, and they collide. Add !(collections & sales) to its constraints.
feature:xray --validate runs the declaration rules without Flask, but not the collision rules.
Reading the source assumes every branch of an
ifin a class body runs, and that an unknown decorator keeps the@typing.overridemark. The boot still checks the live classes.
Seeing who refines what
splent feature:xray
splent_feature_notes (refined by splent_feature_notes_tags)
service: NotesService ← overridden by splent_feature_notes_tags
model: Notes ← extended by splent_feature_notes_tags
hook: notes.create.form_extra ← splent_feature_notes_tags
--full shows every extensible point, not only the ones in use.
See also
- Extension points, declaring what can be overridden
- Refinement, the override mechanism
product:validate, the full product validation