Skip to content

Multi-source model

A catalog.config.json can list more than one sources[] entry — one index.ocx.sh, one corporate mirror, one local directory for testing — and the build merges them. What "merges" means is more specific, and more limited, than it sounds.

Every source is mirrored, unconditionally

mirrorSources() (src/sources/mirror.ts) copies every configured source's wire tree, byte-verbatim, to dist/index/<label>/** — this happens for every source in sources[], root or not. root: true on one source is an additional copy of that same tree at dist/ itself (src/sources/mirror.ts's own doc comment calls this "legacy-compat: today's index.ocx.sh deploy shape, root-relative wire paths"). It is never a substitute for the index/<label>/ copy — a source that isn't root: true still lands in full under its own label prefix.

One merged grid catalog

resolveCatalog() (src/build/sources_pipeline.ts) builds the single /data/catalog/catalog.json the theme's useCatalog.ts fetches, by walking every source in sources[] config order and keeping every package each one publishes.

Nothing is dropped. If two sources both publish acme/widget, both copies appear in the grid, as two cards, linking to two different pages. That is newer behaviour: the merge used to keep only whichever source came first in sources[] and silently discard the rest, because both copies would otherwise have claimed one detail-page route. Index-qualified routes (below) removed that constraint, and a catalog that quietly lost a mirror's package was never an honest rendering of an aggregation.

The merged list is then sorted by package id, with the index name breaking ties — so the two acme/widget cards sit next to each other rather than depending on config order.

Which index a package came from

Every package's name is the fully qualified one its own index published — ocx.sh/hashicorp/terraform, corp.example/platform/deploy-kit — and its first /-segment is the index. src/sources/labels.ts derives a source's label from exactly that segment, and rejects a config whose explicit label disagrees with it (LABEL_PREFIX_MISMATCH), so there is exactly one name for one index and every surface can print it.

That is why cards and table rows carry no badge, dot or colour marking their origin: the identity line already names the index as part of the package's own name. Long names elide in the middle (ocx.sh/…/terraform-provider-aws), never at the end, so both the index and the package survive.

The catalog JSON also carries an indexes envelope — each index's name, whether it is the root index, whether it is the default one, and how many packages it contributes after the merge:

{
  "generated": "…",
  "indexes": [
    { "name": "ocx.sh", "root": true, "default": true, "excludeFromAll": false, "count": 46 },
    { "name": "corp.example", "root": false, "default": false, "excludeFromAll": false, "count": 13 }
  ],
  "packages": [ … ]
}

excludeFromAll is the source's own flag, passed through: when true, the theme's all tab omits that index's packages (its own tab and routes are unaffected). root and default answer different questions, and a config sets them independently:

field question consequence
root is this source mirrored at the site root? its packages keep bare /<ns>/<pkg> routes; every other index qualifies
default which index does the catalog open on? that tab is preselected on arrival and badged default; no route changes

default is already resolved in the envelope: it is the source that set default: true, or — when no source did — the root: true one, which is what every config written before the key existed means. At most one entry carries it; a catalog whose config named neither opens on all.

The pairing matters for a deployment with no root source at all: nothing is served at the site root, every route is qualified — and one of the indexes is still the one an arriving visitor should land on. root: true cannot say that without also moving every URL.

It is written for every catalog, one source included — the theme needs it to resolve a package's route, and a single non-root source has qualified routes just as an aggregating one does. "This deployment aggregates nothing, so show no scope control" is then one fact about the data (indexes has a single entry), not a second predicate kept in sync with the first.

Detail-page routes are index-qualified

The root: true source's packages keep the bare path they have always had:

source qualified name route
root: true ocx.sh/hashicorp/terraform /hashicorp/terraform
any other corp.example/platform/deploy-kit /corp.example/platform/deploy-kit

Note that this table reads root, never default: marking an index default changes which tab opens, never where a page lives.

For a non-root index the route is the qualified identifier, so a copied package link and a copied identifier are the same string, and two indexes publishing the same <namespace>/<package> are never ambiguous.

One consequence worth knowing at config time: a non-root index may not be named after a namespace the root source publishes, since both would claim /<that name>/**. The build rejects that pair by name (INDEX_NAMESPACE_COLLISION) rather than letting one silently overwrite the other.

Each package page carries its own wire identity (ns/pkg) and its source's mount prefix (wireBase — "" for the root source, index/<label> for any other) in its frontmatter, which is how a client-side fetch on a non-root index's detail page reaches the tree mirrorSources() actually wrote.