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 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 itspresence(mandatory/optional),org,package, and group membership;model.alternative_groups, withowner,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
splentCLI, 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_TOKENis 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.