A federation of bundles
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
Section titled “Domains as manifests”The release. A repository that installed the sidecar releases its bundle through the template workflow 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).
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, written as the librarian’s recipe 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 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
Section titled “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.
Four rules keep a tree of domains sound.
- Every domain bounds a workspace of its own. A build knows nothing of a domain above its top domain. A
MemberBundlewhose zip holds aDomainconcept is followed as aMemberDomainis, and the lock marks it. - 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 says a role is held.
- 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.
- The depth is the top domain’s to set.
max_depthon the topDomainconcept 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). 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
Section titled “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.
As a flow diagram, drawn with Archify: light, dark.
-
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). -
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.
-
Verify. Each zip is fetched over http or https from a public address, checked against its
.sha256, and on GitHub against its attestation withgh 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 theconcepts.jsonldits 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.
-
Register and export. Each release is unpacked, or written out from its
concepts.jsonld, then registered withlokf registry addand exported withlokf export. Registering it counts its triples and checks that its concepts sit under its declaredbase_iri. The job exports every bundle under LOKF core, so the named graphs share one vocabulary for the gate andqueries/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. -
Load. Each
graph.ntgoes into a named graph whose IRI is the bundle’sbase_iri, as section 11.4 of the LOKF specification intends. In SPARQL,GRAPH ?gthen says which bundle asserted a triple. -
Gate.
lokf validate --check-refsresolves 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 whoseunowned_targetssaysreportlists 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. -
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 says how to read it each way.
Trust at read time. The snapshot stores no trust label (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). So a concept reads as past its review date the day after, with no rebuild.
What exists, and what remains a design
Section titled “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; the fifth role; the first host, 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) |
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) |
| 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
Section titled “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 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
MemberDomainconcept 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_iriconstant 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). - 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 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).
- 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 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
sensitivitygate of section 11.5 of the LOKF specification is the build’s: a member concept sayspublic,internalorconfidential, public unless set. An internal member joins once a person confirmssensitivity: internalon 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
Section titled “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