product:validate

Table of contents

  1. Introduction
  2. The eight phases
  3. Running one phase
  4. Failing on menu warnings
  5. Checking a candidate before you publish it
  6. Trying another selection
  7. Gating a pipeline
  8. Fixing what it reports
    1. Fixing an unsatisfiable selection
    2. Fixing a version floor
    3. Fixing colliding refiners
    4. Fixing a menu key with no entry
    5. Fixing a parameter value
    6. Fixing a frontend finding
    7. Fixing a security finding
  9. Advanced
  10. Reference
    1. Options
    2. Phase 1. Configuration
    3. Phase 2. Compatibility
      1. Version floors
    4. Phase 3. Dependencies
    5. Phase 4. Refinement
    6. Phase 5. Menus
    7. Phase 6. Parameters
    8. Phase 7. Frontend
    9. Phase 8. Security
  11. Requirements
  12. 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

The eight phases of product:validate and its verdict EIGHT PHASES, ONE VERDICT 1 Configuration config 2 Compatibility compat 3 Dependencies deps 4 Refinement refinement 5 Menus menus 6 Parameters params 7 Frontend frontend 8 Security security Verdict exit 0 or 2 warns, fails with --strict
Every phase must pass. Menus only warns, unless you pass --strict.
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:resolve has 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


Back to top

splent. Distributed by an LGPL license v3. Contact us: drorganvidez@us.es