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 in the catalog: name, description, the uvl pointer (UVLHub DOI/file from metadata.toml) 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_groupsowner, kind, members;
  • model.constraints — requires-implications as [a, b] pairs (a => b).

collisions

Computed across all indexed features: 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: 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, shallow-clone splent_catalog, then one command: splent marketplace:index --registry registry.toml --output public/index.json, published to GitHub Pages.
  • Auth — the workflow’s GITHUB_TOKEN is only used to read repositories at API rate limits; nothing is written back.
  • UVL models — the catalog tracks metadata.toml (DOI pointers), not the .uvl files: those are fetched from UVLHub (spl:fetch) during the build, keeping UVLHub the single remote source of truth for variability models.

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, 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