Skip to content

OpenMoca.Reader

The MOCA Reader for .NET: open a package from a directory, a .moca archive or a host-supplied source; check it; compute its digest; verify attestations and evidence; resolve members; answer structure operations; produce citation records; and search with one entry point over lexical, dense-sidecar or store backends.

It implements the Reader class of the Reader contract, follows the Reader interface, and passes the whole conformance corpus. The reference Reader is moca-reader-typescript.

Terminal window
dotnet add package OpenMoca.Reader

.NET 10. Getting started takes you from install to a cited search result and a conformance run in five steps. The framework bindings are separate packages: add only the ones you use. Example packages are in moca-spec, and moca-examples has a command line and a chat built on this Reader.

The built-in lexical backend is BM25 (k1 1.2, b 0.75) over node text, or over sidecar chunks when a sidecar is bound. Every MOCA Reader tokenises the same way, so they return the same results:

  • NFKC-normalise, then lower-case each code point (simple case mapping).
  • Letters, digits and combining marks make words; anything else separates them. Words in scripts with vowel signs, such as Devanagari, stay whole.
  • Chinese, Japanese, Thai, Lao, Khmer and Myanmar text is split into each character (with its marks) and each pair of neighbouring characters.
  • No stemming. A host that needs it supplies its own lexical backend.
  • Without a locale, every language of a node is searched and its best match returned; with one, the representation package spec §5 selects.

Characters added in very recent Unicode versions can tokenise differently across Readers, because each language runtime ships its own Unicode data.

As Reader contract §14 asks:

Class Reader
Contract version 0.6.0
Implemented Archives, host sources, DSSE attestations, sidecars (lexical and dense), the ontology profile
Sigstore Offline only, with the OpenMoca.Reader.Sigstore package (ReadOptions.SigstoreVerifier = new SigstoreVerifier()), against the trust root’s sigstore.trustedRoot. Without the package, or without a trusted root, a *.sigstore.json attestation is unverifiable (A003). There is no online mode: refresh the trusted root yourself (moca-spec attestations §6)
using OpenMoca.Reader;
var result = await MocaReader.ReadPackageAsync(PackageTarget.FromPath("support-kb"));
Console.WriteLine($"{result.Digest} [{string.Join(", ", result.Capabilities)}]");
var library = new Library().Add(result);
foreach (var record in await library.SearchAsync("refund window"))
Console.WriteLine($"{record.Node.Title} ({record.Node.Ref})");

A malformed package never throws: result.Valid is false and result.Diagnostics says why. The Reader never reaches the network, or any file outside the package, because a package names it.

src/OpenMoca.Reader/Schemas/ holds byte-identical copies of moca-spec’s schemas/v1, checked by the tests.

Separate packages, each a thin adapter over the Reader:

Package What it gives you
OpenMoca.Reader.Onnx OnnxEmbedder and ModelStore: MOCA’s four standard embedding models run locally, as the Reader’s embedder, matching indexes built with moca-index --model exactly
OpenMoca.Reader.Extensions.AI EmbeddingGeneratorEmbedder: any Microsoft.Extensions.AI embedding generator as the Reader’s embedder
OpenMoca.Reader.AgentFramework MocaContextProvider: grounds a Microsoft Agent Framework agent in your packages each turn, and offers moca_search, moca_get_node and moca_structure, as moca-mcp names them
OpenMoca.Reader.Sigstore SigstoreVerifier: verifies Sigstore-signed attestations offline against the host’s trusted root: certificate chain, certificate-transparency timestamp, transparency-log entry and checkpoint, and the DSSE signature. No dependency beyond .NET
OpenMoca.Reader.VectorData VectorStoreBackend<TKey>: a store backend over any Microsoft.Extensions.VectorData collection, with the Reader’s scores and retrieval policy
using OpenMoca.Reader.Onnx;
// A standard model from the model store (~/.moca/models; `moca-mcp models add bge-small-en-v1.5` installs it).
// Each file is checked against its pinned SHA-256 before loading; nothing is downloaded here.
var store = new ModelStore(ModelStore.DefaultFolder);
using var embedder = store.Load(StandardModel.Find("bge-small-en-v1.5")!);
var hybrid = new HybridBackend(new LexicalBackend(library), new DenseBackend(library, embedder));
using OpenMoca.Reader.Extensions.AI;
// e5 models expect a prefix for each role; the embedder applies it, the Reader never does.
var embedder = new EmbeddingGeneratorEmbedder(generator, name: "multilingual-e5-small", dimensions: 384,
queryPrefix: "query: ", documentPrefix: "passage: ");
var dense = new DenseBackend(library, embedder);
using OpenMoca.Reader.AgentFramework;
var search = new MocaSearch(library, new LexicalBackend(library), audiences: ["public"]);
var agent = new ChatClientAgent(chatClient, new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "Answer from the packages and cite each ref." },
AIContextProviders = [new MocaContextProvider(search)],
});

Each turn the provider searches on the user’s message and adds the results as one user-role message: compact citation records as JSON, with a note that they are reference material, never instructions (Reader contract §12). The whole records are in the message’s AdditionalProperties["moca.citations"]. Every tool call goes through the same MocaSearch, so the model cannot widen its audience filter.

using OpenMoca.Reader.VectorData;
// Any connector: CommunityToolkit.VectorData.InMemory, PgVector, Qdrant, SqliteVec, ...
var collection = vectorStore.GetCollection<string, MocaStoreRecord<string>>(
"moca", MocaStore.Definition<string>(embedder.Dimensions));
var store = await new VectorStoreBackend<string>(collection, embedder).IngestAsync(library);
// Search without loading the packages: keep each loaded package's state (PackageState.From(result), plain JSON)
// beside the store. Only those packages are returned, with supersession, contest, signatures and reviews
// recomputed from the states at query time (Reader contract §9.4).
var search = new MocaSearch(packageStates, () => DateTimeOffset.UtcNow, store, audiences: ["public"]);

The store never embeds: the backend embeds through your IEmbedder, by role, and scores each hit itself, so every connector gives the same scores. Each record holds the whole citation record (moca_record) beside the flat fields it filters on, named as in the consuming guide (moca_package_digest, moca_node_id, moca_audience, moca_concepts, …). If a connector cannot translate the filters, pass filterPushdown: false; the gate still applies them. Keys are strings or GUIDs made from each unit, so ingesting a package again replaces it, and RemoveAsync(digest) drops it.

You need the .NET 10 SDK. The tests read moca-spec’s examples and run its conformance corpus. Check out moca-spec beside this repository, or set MOCA_SPEC_DIR. CI uses the tags pinned at the top of .github/workflows/test.yml (MOCA_SPEC_REF and the like); check out the same tags to match it.

Terminal window
dotnet build -c Release
dotnet test --project tests/OpenMoca.Reader.Tests -c Release
dotnet test --project tests/OpenMoca.Reader.Bindings.Tests -c Release

moca-spec’s runner can also drive this Reader through the runner protocol, from a moca-spec checkout:

Terminal window
node scripts/run-conformance.mjs --reader "dotnet run --project ../moca-reader-dotnet/tests/OpenMoca.Reader.ConformanceAdapter -c Release"

The corpus checks what a search returns, never its order. This Reader also promises to rank exactly as the reference Reader does. The parity check runs search scenarios over moca-spec’s examples (lexical, dense, store, hybrid, diversified; sidecars with and without text, zipped; locales and audiences) through both Readers and fails on any difference in records, order, spans, text or scores. Dense sidecars use a deterministic hashing embedder, so no model is needed. With moca-reader-typescript and moca-tools (after npm ci) beside moca-spec:

Terminal window
node tests/OpenMoca.Reader.Parity/parity.mjs
  1. Add a dated section to CHANGELOG.md and update <Version> in src/Directory.Build.props, which every package shares.
  2. Point the MOCA_*_REF pins in .github/workflows/test.yml at the release tags (moca-spec docs/releasing.md).
  3. The tests pass.
  4. Tag vX.Y.Z and publish a GitHub release whose notes are the changelog section.
  5. dotnet pack -c Release from the tagged commit, and publish the four packages to NuGet: OpenMoca.Reader first, then its bindings.

Apache-2.0. See LICENSE and NOTICE.

OpenMOCA™ is a trade mark of PI & Mash Solutions Ltd. The licence covers the code, not the name: see the trade mark policy.