Architecture
How the index is shaped, built and consumed — for contributors to the marketplace itself.
Table of contents
- The index document
- The build pipeline
- Local-first resolution
- Dependency kinds
- 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 itspresence(mandatory/optional),org,package, and group membership;model.alternative_groups—owner,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
splentCLI, shallow-clonesplent_catalog, then one command:splent marketplace:index --registry registry.toml --output public/index.json, published to GitHub Pages. - Auth — the workflow’s
GITHUB_TOKENis only used to read repositories at API rate limits; nothing is written back. - UVL models — the catalog tracks
metadata.toml(DOI pointers), not the.uvlfiles: 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.