Architecture

How the index is shaped, built and consumed, for contributors to the marketplace itself.


Table of contents

  1. The index document
    1. features
    2. spls
    3. collisions
  2. The build pipeline
  3. Local-first resolution
  4. Dependency kinds
  5. The guardian, check:contracts

The index document

index.json is a single self-describing document ("schema": 1) with build metadata (generated_at, generator, sources) and three collections.

{
  "schema": 1,
  "generated_at": "2026-07-26T15:58:56+00:00",
  "generator": "splent marketplace:index",
  "sources": ["..."],
  "features": [ ... ],
  "spls": [ ... ],
  "collisions": [ ... ]
}

features

One entry per feature, keyed by id (org/repo) with a short name. Each entry is the full contract at the latest released tag plus what only the indexer can compute.

Group Fields
Identity id, org, repo, short, version (latest tag), project_version, source
Presentation description, archetype, category, tags, env
Contract provides (routes, blueprints, models, services, hooks, signals, commands, translations, docker), requires (features, features_optional, env_vars, signals), extensible, docker, refinement
Tooling cli_version, requires_python, dependencies
Computed used_by (reverse dependencies across the whole index), pypi (publication state), github

spls

One entry per product line the build can resolve, with name, description, the uvl pointer (mirror, doi, concept_doi, version, file) and the parsed model, so consumers never need a UVL parser.

  • model.features, each feature with its presence (mandatory/optional), org, package, and group membership;
  • model.alternative_groups, with owner, kind, members;
  • model.constraints, requires-implications as [a, b] pairs (a => b).

collisions

Computed across all indexed features, with entries of kind (route, service, model), the colliding item, and the features that provide it. Two colliding features can only coexist in an SPL behind an alternative group. The web sheet and the configurator both surface this.

The build pipeline

The published index lives in splent-io/splent_index. It holds registry.toml (the curated source list) plus a GitHub Actions workflow. The repo contains no data, only the recipe.

  • Cadence. On every push, every 6 hours, and on demand (workflow_dispatch).
  • Build. Install the splent CLI, then run one command, splent marketplace:index --registry registry.toml --output public/index.json, published to GitHub Pages. There is no catalog repository to clone.
  • Auth. The workflow’s GITHUB_TOKEN is only used to read repositories at API rate limits; nothing is written back. No marketplace login is involved, because indexing only reads.
  • UVL models. The indexer resolves each model the same way the CLI does, from a working copy, then from .splent_cache/spls/, then by downloading it from UVLHub by DOI. A model it cannot fetch is indexed without one and warned about, never a build failure.

The full option set (orgs, explicit repos, local mode) is documented at marketplace:index.

Local-first resolution

Every consumer resolves the index the same way, freshest copy first with graceful degradation always.

Consumer Order
CLI (feature:search / feature:info) Workspace cache (.splent_cache/marketplace/index.json) → SPLENT_INDEX_URL → live GitHub fallback
Marketplace web feature In-memory TTL cache → workspace cache → SPLENT_INDEX_URL → last stale copy → empty index
Configurator feature MarketplaceService (soft dependency) → workspace cache → SPLENT_INDEX_URL (caching what it fetches)

The workspace cache wins on purpose. A development checkout’s freshly built --local index may describe features and SPLs that are not published yet, while production containers have no workspace cache and read the published index.

Dependency kinds

The contract distinguishes three kinds of feature dependency, and the distinction is inferred from how the code is written.

Kind Contract field Inferred from Enforced?
Hard requires.features Imports of other features, and bare service_proxy("XService") usage, code that breaks without the provider Yes, feature:install resolves them recursively; feature:remove blocks removal
Soft requires.features_optional service_proxy(...) wrapped in try:, the codebase idiom for graceful degradation No, shown by the marketplace as “works better with”
Manual requires.features_manual Declared by the author for real dependencies static analysis cannot see (e.g. one feature storing data through another’s admin UI); preserved across contract regeneration and merged into requires.features Yes, same as hard

This is why the rule of thumb matters. Use service_proxy bare when the feature genuinely needs the service, and wrap it in try/except when the feature merely improves with it. The contract generator turns that choice into the marketplace’s dependency graph. (The marketplace’s own Elasticsearch upgrade and the configurator’s use of MarketplaceService are both soft dependencies.)

The guardian, check:contracts

A dependency lives in two places, the contract (what feature:install and the marketplace see) and the SPL’s UVL constraints (what configuration validity sees). check:contracts verifies both tell the same story, erroring on contract dependencies with no UVL constraint and warning on UVL implications no contract backs. Run it in CI next to product:validate to keep the index and the variability models honest with each other.


Back to top

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