product:validate
Table of contents
- Introduction
- The eight phases
- Running one phase
- Failing on menu warnings
- Checking a candidate before you publish it
- Trying another selection
- Gating a pipeline
- Fixing what it reports
- Advanced
- Reference
- Requirements
- See also
Introduction
product:validate checks that your product is a valid configuration of its SPL. It runs eight phases, from the UVL selection to the SQL and XSS sinks in your features’ code, and gives one verdict. Run it before you derive, build or release, and in CI.
splent product:validate
Validating my_first_app...
1. Configuration
✅ Feature selection is satisfiable (9 features)
2. Compatibility
✅ No collisions detected
3. Dependencies
✅ All imports respect UVL constraints (12 checks)
4. Refinement
✅ No refiners in this product
✅ The SPL has no refiners
5. Menus
✅ [tool.splent.layout.menu] names no keys
6. Parameters
✅ Every value checked fits its feature's schema (3 values)
7. Frontend
✅ No feature of this product has frontend/
8. Security
✅ No SQL or XSS sink in the features' code and templates (9 features)
✅ Product is valid.
It exits 0 when the product is valid and 2 when it is not.
The eight phases
| Phase | --only |
Checks that |
|---|---|---|
| 1. Configuration | config |
The feature selection satisfies the UVL model |
| 2. Compatibility | compat |
The features’ contracts do not collide, and every version floor holds |
| 3. Dependencies | deps |
A feature imports another only when the model says it depends on it |
| 4. Refinement | refinement |
The refiners agree with each other and with their bases |
| 5. Menus | menus |
Every key of [tool.splent.layout.menu] has a menu entry |
| 6. Parameters | params |
Every value of [tool.splent.config] fits its feature’s schema |
| 7. Frontend | frontend |
Each feature’s bundle finds on the page what it needs, and ships only what it may |
| 8. Security | security |
No feature builds SQL or markup from a variable string, or has a template that runs inline code |
A menu table the app would refuse fails phase 5 even without --strict.
Running one phase
splent product:validate --only params
Use it while you fix one kind of problem. Any phase name from the table above works.
Failing on menu warnings
splent product:validate --strict
Phase 5 warns about a menu key no entry has, and about keys it could not check. With --strict those fail the product.
Checking a candidate before you publish it
splent product:validate --pyproject <candidate.toml>
splent product:validate --model <candidate.uvl>
--pyproject validates another pyproject.toml, such as your next pins, in every phase. The releases it pins must already be in the cache.
--model validates against another UVL model, such as the next version of the line, in phases 1, 3, 4 and 6. The model’s working copy is left alone.
Trying another selection
splent product:validate --only config --features auth,profile,notes --print-config
--features checks this list in phase 1, instead of your pyproject.toml. --print-config prints the 0/1 assignment the solver received, parameter nodes included.
Gating a pipeline
- name: Validate the product
run: splent product:validate --strict
An invalid product exits with 2, so the job stops.
product:derive, product:build and product:release already run phases 1 and 2, the product part of phase 4 and the errors of phases 6 and 7 as their pre-flight. They stop on an error.
Fixing what it reports
Fixing an unsatisfiable selection
❌ Configuration is NOT satisfiable under UVL constraints
the constraint notes_most_tagged => notes_tags does not hold: notes_most_tagged is selected, notes_tags is not
Phase 1 names every rule your selection breaks. Add the feature the rule asks for, or remove the one that needs it. product:missing lists what the model requires and your product lacks.
Fixing a version floor
❌ [requires] reports requires notes>=1.20.0, but the product pins notes at v1.19.0. Pin a release of notes that satisfies it.
A feature’s contract can require a minimum release of another. Pin a newer release of the required feature.
Fixing colliding refiners
❌ [duplicate-override] splent_feature_bundles and splent_feature_sales both override the template shop/list.html of splent_feature_shop. Only one refiner may override it. Keep the override in one of them, or declare them incompatible in the UVL with !(bundles & sales).
Do what the message says. Keep the override in one refiner, or declare the two incompatible in the model.
Fixing a menu key with no entry
⚠️ noets (hide) has no entry: no feature of the product registers it
A misspelt key in [tool.splent.layout.menu], or one of a feature that left the product, hides nothing. Fix the key or remove it.
To find the entries, phase 5 builds your app. When the CLI container lacks a package your features need, it uses the product’s web container, so start it with splent product:up --dev.
Fixing a parameter value
❌ NOTES_MAX_ATTACHMENT_SIZE (notes): expected a size (100M, 1G or a number of bytes), got '20 megas'
Fix the value so it fits the feature’s schema. A secret never belongs in pyproject.toml, so set it in the deployment’s .env.
Fixing a frontend finding
❌ [F-PEER-RANGE] wizard needs bootstrap ^5.3.0 (peerDependency); my_first_app provides 5.2.0.
Phase 7 compares what each feature with frontend/ takes from the page with what your shell provides. Declare the shell in your product’s package.json.
{
"name": "my_first_app",
"splent": {
"schema": 1,
"provides": {
"bootstrap": { "kind": "lib", "global": "bootstrap", "version": "5.2.0" },
"bootstrap-css": { "kind": "css", "version": "5.2.0" },
"icons": { "kind": "icons", "set": "feather" }
}
}
}
Without it, the shell is read from src/<product>/static/js/app.js. The codes are listed below.
Fixing a security finding
❌ notes routes.py:42 [sql-from-string] text() takes SQL built with an f-string
Bind the values, text("... WHERE id = :id").bindparams(id=value), or use the ORM.
The framework escapes what a template prints and binds what the ORM sends. Phase 8 finds the code that gets around both, SQL or HTML built from a string, and the template code the Content-Security-Policy blocks. The fix is printed under each finding.
# Refused
db.session.execute(text(f"SELECT id FROM notes WHERE title = '{title}'"))
return Markup(f'<a href="{url}">{name}</a>')
# Fine
db.session.execute(
text("SELECT id FROM notes WHERE title = :title"), {"title": title}
)
return Markup('<a href="{}">{}</a>').format(url, name)
An order the request names goes through a mapping of the columns it may pick.
from splent_framework.security import sort_by
notes = sort_by(
Note.query,
request.args.get("sort"),
{"title": Note.title, "created": Note.created_at},
)
In a template, bind events and run code from the feature’s JavaScript. A <script> that must stay inline takes the request’s nonce.
<script nonce="{{ csp_nonce() }}">window.notes = {{ notes | tojson }};</script>
A line you have reviewed and know is safe, such as a table name your code quotes, says so with the rule’s name. feature:security lists the findings of one feature, and feature:release refuses a feature that has any.
conn.execute(text(f"DROP TABLE {quote(name)}")) # splent: allow sql-from-string
<a href="javascript:history.back()">Back</a>{# splent: allow javascript-url #}
Advanced
Each feature is read from the copy the app would load, the editable checkout or the snapshot of the pinned release. A feature with no local copy is reported as not checked. Phase 7 needs no Node or network, so a CI job that ran
product:resolvehas all it needs.
Phase 4 reads the source without running it. An attribute a mixin adds only under a runtime condition can show as a collision the app would not raise.
Reference
Options
| Option | Description |
|---|---|
--only PHASE |
Run one phase |
--strict |
Fail on what phase 5 warns about |
--pyproject PATH |
Validate this pyproject.toml, in every phase |
--model FILE |
Validate against this UVL model, in phases 1, 3, 4 and 6 |
--features "a,b" |
Check this list in phase 1 |
--print-config |
Print the 0/1 assignment of phase 1 |
Phase 1. Configuration
A node of the model is selected when your product lists it in features, or in features_dev or features_prod for the current SPLENT_ENV (dev when unset). A node without a package is also selected when a parameter selects it from your value, or when it is the ancestor of a selected one.
| Line under the verdict | When |
|---|---|
the constraint <c> does not hold: ... |
A cross-tree constraint is broken |
<child> is mandatory under <parent> and is not selected |
A mandatory feature is missing |
<child> is selected and its parent <parent> is not |
A feature is listed without its parent |
<group> (alternative group: ...) has ... |
None, or more than one, is selected |
<group> (or-group: ...) has no selected child |
None is selected |
Phase 2. Compatibility
| Finding | Result |
|---|---|
| Two features register the same route or blueprint name | Error |
| A pinned release below a version floor | Error |
A feature with env = "dev" in features or features_prod, or env = "prod" in features_dev |
Error |
A feature’s services on splent_network instead of ${SPLENT_NETWORK:-splent_network} |
Error |
A feature named after a product, such as auth_uvlhub |
Warning |
Version floors
A contract may require notes>=1.20.0 (see Version floors in a contract). The specifier must be PEP 440. A leading v is dropped, and pre-releases count.
| The required feature is | Result |
|---|---|
| Pinned at a version that meets it | Nothing |
| Pinned at a version that does not | Error |
Pinned at a branch, such as main |
Warning |
| Editable, below the floor or with no version | Warning |
| Missing | Reported as missing, if required |
Phase 3. Dependencies
A feature that imports another without a UVL dependency on it is an error. An import under a guard, or of a feature its contract lists as optional, is not (see check:deps).
Phase 4. Refinement
The phase applies the rules the app applies at start without importing any feature. A collision between your product’s refiners, or a declaration the app would refuse, is an error. Across the SPL, two refiners that collide and that the model allows together are a warning with the constraint to add.
Phase 5. Menus
| Finding | Default | --strict |
|---|---|---|
| A key no feature registers | Warning | Error |
| A key not checked, since a feature that may register it has no local copy | Warning | Error |
| The app could not be built | Warning | Error |
A key only features_dev or features_prod registers |
Note | Note |
| A table the app refuses | Error | Error |
The app is built for the current SPLENT_ENV, and a build over five minutes is given up.
Phase 6. Parameters
| Finding | Result |
|---|---|
A value the schema refuses, or a template nobody ships |
Error |
A required key bound at derivation that the product does not set |
Error |
A required key bound at deploy that no docker/.env.*.example sets |
Warning |
A secret in pyproject.toml |
Error |
| A schema that cannot be used | Error |
| A key no selected feature declares | Error when every feature has a schema, else a warning |
A key of a feature’s own [tool.splent.config] that its schema leaves out |
Warning |
| A key of a feature without a schema | Note |
A uvl = true parameter, or one of its choices, with no node in the model |
Warning |
| A model node for a key or value no schema declares | Warning |
Phase 7. Frontend
| Code | Result | When |
|---|---|---|
F-PEER-MISSING |
Error | A peer or a shell capability nothing provides |
F-PEER-RANGE |
Error | A peer provided out of its range |
F-CONSUMES-RANGE |
Error | A consumed API its provider does not provide in range |
F-PROVIDER-DUP |
Error | Two providers of one name |
F-SINGLETON-BUNDLED |
Error | A bundle ships a singleton the shell or another bundle ships |
F-LICENSE |
Error | A licence the feature may not ship, unless excepted |
F-LICENSE-REVIEW |
Warning | A licence to review |
F-DIST-MISSING, F-DIST-STALE |
Error | A release whose bundle is missing or does not match its manifest |
F-DIST-STALE |
Warning | A working copy built before its sources changed |
F-GLOBAL-UNDECLARED |
Error | A template uses a page global its feature does not declare |
F-LEGACY |
Warning | A feature without frontend/. splent feature:frontend:init moves it |
F-CDN |
Error | A feature with frontend/ loads from another origin. Ship it as an npm dependency or in splent.vendored. A host the feature asks for in splent.csp is fine. A warning in legacy features and product templates |
F-CSP-UNAPPROVED |
Warning | A CSP source asked for and not approved, or approved for a feature the product does not have |
F-CSP-ASKED-IN-CODE |
Info | A source approved for a feature whose splent.csp does not ask for it. The page gets it when the feature asks from its code with csp.ask |
F-CSP-FORBIDDEN |
Error | A CSP source the policy never takes, or a malformed [tool.splent.csp] |
Phase 8. Security
A finding in a feature you edit is an error. One in a feature pinned to a release is a warning, because only a later release of the feature fixes it, and feature:release refuses that release while the finding is there. The phase reads each feature’s Python and its .html, .j2, .xml and .svg files under templates/, and leaves its tests out.
| Rule | Finding |
|---|---|
sql-from-string |
The SQL of text(), execute(), exec_driver_sql() or literal_column(), or a string given to filter(), where(), order_by() or having(), built with an f-string, %, + or format() from a value that is not a literal |
markup-from-string |
Markup() of a string built that way from a value not wrapped in escape(). Markup("...").format() escapes, so it is fine |
template-from-string |
render_template_string() of a template that is not a literal |
autoescape-off |
{% autoescape false %} |
inline-handler |
An event handler attribute, such as onclick= |
inline-script |
A <script> with code and no src or nonce. A data block, such as type="application/json", is fine |
javascript-url |
An href, src, action or formaction that starts with javascript: |
A literal, a module-level constant, a pick from a constant mapping and a local name only ever bound to one of those are not variable.
| Exception | Holds on |
|---|---|
# splent: allow <rule> |
Any line of the Python statement |
{# splent: allow <rule> #} |
The template line, or the line its tag starts on |
Requirements
- A selected product (
SPLENT_APP). - For phase 5, when the menu table names keys, an app that builds here or in the product’s web container.
See also
feature:compatcompares two features.feature:xrayshows refinements and hook usage.product:missinglists the features the model requires and the product does not declare.feature:securitylists the SQL and XSS sinks of one feature.