# A federation of bundles

> [!NOTE]
> This page is for whoever runs a domain host of Knowledge Trust Ladder (KTL), or reads its workspace. [The curated context layer](https://noelmcloughlin.github.io/knowledge-trust-ladder/docs/context-layer/) says what a context layer is and why KTL federates by domains.

A *domain* is a named set of bundles with one accountable owner, kept as a thin LOKF bundle in a repository of its own. A domain may name other domains. The domain a build starts from is that build's *top domain*, and the same domain may be a member of another. *The workspace* is the graph built from the top domain's members, one named graph per bundle.

## Domains as manifests

**The release.** A repository that installed the sidecar releases its bundle through the template workflow [`knowledge-release.yaml`](https://github.com/noelmcloughlin/knowledge-trust-ladder/blob/a70038260880b7d9f47cd7e8ac796b2a2213168e/skills/ktl-sidecar/templates/github/knowledge-release.yaml), as a reproducible zip whose root is `knowledge/`, validated by the registrar's checks and never edited afterwards. Its `.sha256`, a build attestation where the forge offers one, and a domain schema the justfile names are released beside it ([automation.md](https://github.com/noelmcloughlin/knowledge-trust-ladder/blob/a70038260880b7d9f47cd7e8ac796b2a2213168e/skills/ktl-sidecar/references/automation.md#knowledge-releaseyaml-the-bundle-as-a-release-asset)).

**What LOKF specifies.** Section 11 of the LOKF specification, "Federation: registries of bundles", federates bundles in linked-data terms. A bundle publishes the four files `lokf export` writes, `graph.nt` among them. A registry, `lokf-registry.yaml`, is a DCAT catalogue of VoID datasets, one entry per member bundle keyed by its `base_iri`. An IRI resolves to its owner by the longest matching `base_iri`, "pure string arithmetic" with "no network, no shared database", and one no member owns is "a tolerated dangling link, not an error". Member namespaces may not nest: the registry refuses a `base_iri` inside another. The toolkit's `lokf registry init`, `add`, `list` and `resolve` implement the registry offline. The specification marks the federated store of section 11.4 informative, and the sidecar's `knowledge-federate.py` supplies it.

**What a domain adds.** A domain is released the same way as any bundle. Its concepts are the member bundles: each a `Dataset` whose `distribution` names the release zip by `access_url`, whose `version` is the release tag, and whose `isPartOf` names the domain concept. A bundle that releases no zip is named by the `concepts.jsonld` its site serves. A cross-bundle claim is a concept with typed relations whose targets are full IRIs in the members' namespaces: that bundle A's term is bundle C's term, or that A depends on C.

LOKF has no catalogue class, and its `Distribution` takes any checksum or none, so a domain carries a one-file domain schema, the sidecar's [`domain.yaml`](https://github.com/noelmcloughlin/knowledge-trust-ladder/blob/a70038260880b7d9f47cd7e8ac796b2a2213168e/skills/ktl-sidecar/templates/domain.yaml), written as [the librarian's recipe](https://github.com/noelmcloughlin/knowledge-trust-ladder/blob/a70038260880b7d9f47cd7e8ac796b2a2213168e/skills/ktl-librarian/references/domain-schema.md) says. It defines `Domain`, a `Dataset` with `class_uri: dcat:Catalog`; `MemberBundle`, a `Dataset` with `release` and a distribution that must carry a sha256 `checksum`; and `MemberDomain`, a member that is a domain in turn. A reader of the manifest then sees the delegation without unpacking the zip. The same validator checks the schema, and the curator's [domain-bundle.md](https://github.com/noelmcloughlin/knowledge-trust-ladder/blob/a70038260880b7d9f47cd7e8ac796b2a2213168e/skills/ktl-curator/references/domain-bundle.md#the-four-concept-types) gives every field.

Nothing is copied upward: a domain selects releases, so a member's new release leaves no stale copy in the domain. A person's confirmation of a member concept attests two things: that the member belongs, and that the domain's claims across bundles hold. Each member keeps its own labels. The **curator** confirms a member concept as any other, and the **registrar**'s gate ties the confirmation to the person.

## Tiers: a domain of domains

The build follows a member whose released bundle is itself a domain: its members join the same workspace, and the lock says which domain named each. A company's legal structure is one such tree, a legal domain of lines of business, each a domain of business segments. A competence structure is a second tree over the same bundles. Each tier is a repository of its own, with its own curator and releases.

<p align="center">
  <picture>
    <source media="(prefers-color-scheme: dark)" srcset="https://noelmcloughlin.github.io/knowledge-trust-ladder/assets/ktl-federation-tiers-dark.svg">
    <img src="https://noelmcloughlin.github.io/knowledge-trust-ladder/assets/ktl-federation-tiers.svg" alt="Tiers of domains, each box labelled with its domain concept's path and its namespace, https://w3id.org/ then the publisher's w3id namespace, the project and knowledge/: domains/federation, where this build starts and perhaps a member of another domain, names domains/domain1 and domains/domain2; each names a domain in turn, domains/domain3 and domains/domain4; those name the bundles, any number under each, drawn as a stack, and one shared model such as gist-linkml, biolink-model or the FINOS CDM, named by both at one release, so it joins once. Each card carries a small ladder whose rungs are the three actors and three pills reading derived, checked and confirmed, green for an agent, gold for deterministic tools, blue for a named person; each domain box carries the same three dots. Below, four numbered rules: every domain bounds a workspace of its own; confirming a domain member attests that it belongs and who answers for it; the diamond rule; and max_depth, with a note that every namespace takes that form, a domain's beside its members', never above them." width="720" />
  </picture>
</p>

Four rules keep a tree of domains sound.

1. **Every domain bounds a workspace of its own.** A build knows nothing of a domain above its top domain. A `MemberBundle` whose zip holds a `Domain` concept is followed as a `MemberDomain` is, and the lock marks it.
2. **Confirming a domain confirms the delegation.** The upper curator's confirmation of a domain member attests that the domain belongs and who answers for it, not what its members say. Each tier's owner answers for that tier, as [Lines are roles, not people](https://noelmcloughlin.github.io/knowledge-trust-ladder/docs/three-lines/#lines-are-roles-not-people) says a role is held.
3. **The diamond rule.** Two domains may name the same bundle. When both name the same release, the bundle joins the workspace once, and the lock records both domains. When they name different releases, the build stops, because one named graph holds one release. It stays stopped until the two owners agree on one release and both pin it. A model many domains share, an upper ontology such as gist, is the ordinary case: every sub-domain that links to its terms names it.
4. **The depth is the top domain's to set.** `max_depth` on the top `Domain` concept is how many tiers the build follows, 3 unless set. The domain's own members are tier 1. A domain found at the last tier stops the build. A domain may not name itself or a domain above it.

Namespaces may mirror the tree, as long as none nests inside another ([Domains as manifests](https://noelmcloughlin.github.io/knowledge-trust-ladder/docs/federation/#domains-as-manifests)). A path hierarchy passes when each tier's namespace ends in its own segment: `https://corp.example/knowledge/`, `https://corp.example/legal/knowledge/` and `https://corp.example/legal/lob-a/knowledge/` are accepted together, and `https://corp.example/legal/` alone is refused. The gate is the top domain's: one workspace has one `unowned_targets`, and a sub-domain's applies to its own build.

## How the workspace stays current

The workspace is a materialised view of the top domain, built ahead of time and rebuilt on publish. `knowledge-federate.py build` builds it, `knowledge-federation.yaml` runs the build on each release of the domain, and no agent runs in either.

<p align="center">
  <picture>
    <source media="(prefers-color-scheme: dark)" srcset="https://noelmcloughlin.github.io/knowledge-trust-ladder/assets/ktl-context-workspace-dark.svg">
    <img src="https://noelmcloughlin.github.io/knowledge-trust-ladder/assets/ktl-context-workspace.svg" alt="How the context workspace stays current: a publish event in the artifact repository starts a refresh job that resolves, verifies, builds, gates and swaps; inside the workspace a manifest and lock, the released bundle files and one named graph per bundle sit behind a read interface that readers query over MCP or SPARQL." width="720" />
  </picture>
</p>

***As a flow diagram, drawn with Archify: [light](https://noelmcloughlin.github.io/knowledge-trust-ladder/assets/companions/ktl-context-workspace.svg), [dark](https://noelmcloughlin.github.io/knowledge-trust-ladder/assets/companions/ktl-context-workspace-dark.svg).***

1. **A publish event starts it.** On GitHub, the host's release starts the job. An artifact repository's webhook can start it too, through `workflow_dispatch` ([which repositories send one](https://noelmcloughlin.github.io/knowledge-trust-ladder/docs/context-layer-technology/#find-your-case)).
2. **Resolve.** The job reads the top domain's manifest, follows each domain to its members, and writes a lock: one entry per release with its URL, its checksum, its tier and the domains that named it. It takes each member, and the domain's own settings, as a person last confirmed them, and lists a member that waits for a confirmation.
3. **Verify.** Each zip is fetched over http or https from a public address, checked against its `.sha256`, and on GitHub against its attestation with `gh attestation verify`, signed by the member's own release workflow. A zip holding a link, a hidden file or a file a browser runs is refused. A bundle that releases no zip is checked by the checksum of the `concepts.jsonld` its site serves. A file that fails a check stops the job, and the old snapshot stays.

   A member whose file cannot be fetched is held at its last good state, as section 11.5 has it: the job takes the member as the previous release's workspace holds it. A release zip that serves other bytes is held only at the release a build fetched before, since its checksum is then wrong. A member that workspace never held is left out until its file can be fetched, and a pull request's check never holds a member the pull request proposes.

4. **Register and export.** Each release is unpacked, or written out from its `concepts.jsonld`, then registered with `lokf registry add` and exported with `lokf export`. Registering it counts its triples and checks that its concepts sit under its declared `base_iri`. The job exports every bundle under LOKF core, so the named graphs share one vocabulary for the gate and `queries/labels.rq`. It also validates each bundle against the schema released with it, or against LOKF core when it releases none, and the gate report's vocabulary fit section gives the result.
5. **Load.** Each `graph.nt` goes into a named graph whose IRI is the bundle's `base_iri`, as section 11.4 of the LOKF specification intends. In SPARQL, `GRAPH ?g` then says which bundle asserted a triple.
6. **Gate.** `lokf validate --check-refs` resolves only targets inside a bundle's namespace, so a link from one bundle to another is first checked at this step. The job lists every target outside its bundle's namespace and resolves each against the registry. A bundle must tolerate a dangling link, as OKF requires, but in a domain it is a defect, because a person confirmed the claim it belongs to. So an IRI no member owns fails the build, and so does one in a member's namespace that names no concept of that member. A domain whose `unowned_targets` says `report` lists them instead. A pull request's check runs the same gate, so such a link fails before the merge. The links that pass are counted between members, in the lock and the workspace index.
7. **Swap.** The new graphs replace the old under one snapshot version, in one atomic step. A reader never sees half a federation.

**Pre-built is not pre-loaded.** The workspace serves the index first and lets a reader traverse on demand, over a SPARQL endpoint and a Model Context Protocol (MCP) server. The MCP server loads a member's graph only when a walk first reaches its namespace, with every graph that links into it. Each reader opens the workspace in place: an unpacked folder, a `workspace-<tag>.zip`, or the site that serves it, read one graph at a time by range request. That is the job OKF gives `index.md`, "to support progressive disclosure". It is also Anthropic's guidance that agents "maintain lightweight identifiers" and "dynamically load data into context at runtime". The curator's [domain-bundle.md](https://github.com/noelmcloughlin/knowledge-trust-ladder/blob/a70038260880b7d9f47cd7e8ac796b2a2213168e/skills/ktl-curator/references/domain-bundle.md#reading-the-workspace) says how to read it each way.

**Trust at read time.** The snapshot stores no trust label ([Trust stays visible](https://noelmcloughlin.github.io/knowledge-trust-ladder/docs/architecture/#trust-stays-visible)). `queries/labels.rq` computes each one from `generated` and `verified` when the query runs, as the report script does from frontmatter ([the same labels as SPARQL](https://github.com/noelmcloughlin/knowledge-trust-ladder/blob/a70038260880b7d9f47cd7e8ac796b2a2213168e/skills/ktl-curator/references/queries.md)). So a concept reads as past its review date the day after, with no rebuild.

## What exists, and what remains a design

| Part | Where |
| --- | --- |
| The release, with a domain schema beside the bundle | `knowledge-release.yaml` in the sidecar's templates |
| The registry, offline, and the readers over one bundle | lokf 0.9.0: `lokf export`, `registry`, `serve`, `query`, `mcp` and `tables`; specification sections 11.1 to 11.3 |
| Domains, their schema, and a person's confirmation of membership, which the registrar's gate ties to that person | `domain.yaml` in the sidecar's templates; the curator's [domain-bundle.md](https://github.com/noelmcloughlin/knowledge-trust-ladder/blob/a70038260880b7d9f47cd7e8ac796b2a2213168e/skills/ktl-curator/references/domain-bundle.md); [the fifth role](https://noelmcloughlin.github.io/knowledge-trust-ladder/docs/architecture/#the-fifth-role-which-is-not-a-skill); the first host, [ktl-federation](https://w3id.org/ktl/federation/) |
| The build and its gate, and the proposals that move a member | `knowledge-federate.py build` and `members`, which `knowledge-federation.yaml` and the librarian's workflow run ([automation.md](https://github.com/noelmcloughlin/knowledge-trust-ladder/blob/a70038260880b7d9f47cd7e8ac796b2a2213168e/skills/ktl-sidecar/references/automation.md#knowledge-federationyaml-the-workspace-of-a-domain-host)) |
| The readers over the workspace: the `lokf-registry` MCP server of section 11.4, a SPARQL endpoint, a query, and what changed between two workspaces | `knowledge-federate.py mcp`, `serve`, `query` and `diff`; the Oxigraph server image ([domain-bundle.md](https://github.com/noelmcloughlin/knowledge-trust-ladder/blob/a70038260880b7d9f47cd7e8ac796b2a2213168e/skills/ktl-curator/references/domain-bundle.md#reading-the-workspace)) |
| The workspace view | `graph.json` and `queries/links.rq`; the website skill's graph page |

Of section 11, one part remains a design: fetching a member from a private repository, which section 11.5 leaves to a later version, since credentials never enter the manifest. Such a bundle joins from a local path, in a build a person runs with `--allow-local`. Per-query permission for many readers of one store stays with that store and the artifact repository. Each member is one named graph, so a store grants a reader the members it may see, and a reader started with `--member` reads those members' graphs alone.

## Decisions an adopter must make

- **Pin or float.** This design pins: each member concept names one release, and a person's confirmation of the domain covers those releases. A member's new release changes nothing until a person confirms the member concept that names it and the domain releases, so a pinned domain releases for every member release. A bundle that releases no zip is pinned by the checksum of the file its site serves, and held as [step 3](https://noelmcloughlin.github.io/knowledge-trust-ladder/docs/federation/#how-the-workspace-stays-current) says when its site serves another file before the domain releases. A manifest that floats on a range is fresher, but its curator has not seen each release it picks up. The lock then holds the exact releases, as a package manager's lockfile does. Pinned or floating, the report must say, at each tier, which members have moved since the domain was last confirmed.
- **Pin across tiers.** Pinning compounds: a leaf's release reaches the top workspace only after a confirmation and a release at every tier between. The templates pin at every tier. The open question is whether a domain member may float on its sub-domain's newest release, so that an upper tier's confirmation attests membership and ownership while leaf members stay pinned. Until that is decided, a member that waits in a member domain's release joins at the release that domain's own build took. The lock in that domain's released workspace records it, and the `MemberDomain` concept names the workspace and its checksum. Without that workspace, the member is left out above, because a zip carries no history to find the release confirmed before.
- **Stable IRIs.** Keep each bundle's `base_iri` constant across releases and record the version on the graph's metadata, or every cross-bundle edge breaks at each release. Give a domain a namespace beside its members', never above them ([Tiers](https://noelmcloughlin.github.io/knowledge-trust-ladder/docs/federation/#tiers-a-domain-of-domains)).
- **Who arbitrates.** When A and C define one term differently, the domain's owner decides, and the decision is a confirmed concept in the domain bundle, not a rewrite of either member. Atlan's "Conflicts get arbitrated, not duplicated" names the duty, and [Lines are roles, not people](https://noelmcloughlin.github.io/knowledge-trust-ladder/docs/three-lines/#lines-are-roles-not-people) says who may hold it.
- **Vocabulary fit.** A concept whose type is outside the schema is tolerated as a generic concept and carries no agreed meaning. Admit a bundle to a domain when its vocabulary fits, and publish each domain schema as an artifact so the workspace can load it ([when the vocabulary stops fitting](https://noelmcloughlin.github.io/knowledge-trust-ladder/docs/for-the-curious/#when-the-vocabulary-stops-fitting)).
- **What a member adds.** A domain admits a bundle on its admission rule: released with the sidecar's workflow, validating against a published schema, a stable namespace under an authority its publisher controls, one accountable owner. Value is a stricter test: readers query across the member, or other members link into it. A federation whose members never link to one another has proved its mechanism, not its worth, and [step 6](https://noelmcloughlin.github.io/knowledge-trust-ladder/docs/federation/#how-the-workspace-stays-current) counts those links at each release. A public bundle cannot decline to be catalogued, and the domain's owner answers for the admission.
- **Permissions.** OKF is explicit: "Trust tiers are advisory signals, not access control." The `sensitivity` gate of section 11.5 of the LOKF specification is the build's: a member concept says `public`, `internal` or `confidential`, public unless set. An internal member joins once a person confirms `sensitivity: internal` on the Domain concept, and only in a private or internal repository, whose release then carries the workspace and whose site does not. A confidential one joins no workspace. The domain's own site still shows its member concepts, so an internal member's concept says no more than that site may. A bundle that must not be seen whole does not join a domain.

## Sources

- Alexander, K., Cyganiak, R., Hausenblas, M. and Zhao, J. (2011). Describing Linked Datasets with the VoID Vocabulary. W3C Interest Group Note. <https://www.w3.org/TR/void/>
- Anthropic (2025). Effective context engineering for AI agents. <https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents>
- Atlan (2026). Context layer. Atlan Documentation. <https://docs.atlan.com/agents/concepts/context-layer>
- GitHub. Artifact attestations. GitHub Docs. <https://docs.github.com/en/actions/concepts/security/artifact-attestations>
- Google Cloud (2026). Open Knowledge Format, specification version 0.2. <https://github.com/GoogleCloudPlatform/open-knowledge-format/blob/main/SPEC.md>
- Harris, S. and Seaborne, A. (2013). SPARQL 1.1 Query Language, section 13. W3C Recommendation. <https://www.w3.org/TR/sparql11-query/>
- Nichols, N. (2026). LOKF specification, version 0.2, section 11: Federation, registries of bundles. <https://lokf.nolan-nichols.com/specification/#11-federation-registries-of-bundles>. The toolkit as released at 0.9.0: <https://github.com/nicholsn/lokf/blob/v0.9.0/SPEC.md>
