MOCA Reader Contract
Specification version: 0.6.0
Status: Before 1.0.0. A minor version may still change the format; each change is in MIGRATIONS.md.
License: Apache License 2.0
1. Scope
Section titled “1. Scope”This document says what any software that reads MOCA packages must conclude about a package and how it must behave, in any language. It specifies behaviour, not API shape: method names, types and error handling follow each language’s own idioms. Where this document and the package specification disagree, the package specification takes precedence.
Conformance is shown by passing the conformance corpus.
The reference implementation is @openmoca/moca-core.
The operations every implementation offers, named once for all languages, are
in the Reader interface.
The key words MUST, MUST NOT, REQUIRED, SHOULD, SHOULD NOT, RECOMMENDED, MAY and OPTIONAL are to be interpreted as described in RFC 2119 and RFC 8174 when, and only when, they appear in capitals. Terms are as defined in package specification §2.
2. Conformance classes
Section titled “2. Conformance classes”| Class | Does | Typical software |
|---|---|---|
| Reader | Opens, checks and verifies packages, produces citation records, searches with the default retrieval policy applied, and answers structure operations (§3-§13). | Language Readers, the MCP server, framework bindings. |
| Producer | Everything a Reader does, plus writing packages, attestations or sidecars that a Reader accepts. | Converters, signing tools, index builders. |
A Producer MUST NOT write anything that a Reader of the same contract version (the release version of the document set, package spec §14) would reject with an error.
Search is part of the Reader class: anything that hands MOCA content to an application or a model goes through a Reader, so the corpus can show that it never returns what §8 excludes.
3. Opening a package
Section titled “3. Opening a package”A Reader MUST accept a directory with moca.json at its root. It SHOULD accept
a .moca Zip archive (as a path or as bytes) and a package source: an
object the host implements that lists a package’s files and returns the
bytes of any listed file, so packages can live in memory, blob storage or a
database. The same package MUST produce the same conclusions, including the
same digest, whatever its source.
Every source has the same limits. Before reading any entry of an archive,
directory or host source, a Reader MUST reject it (T003_ARCHIVE_REJECTED) if
it has more entries, or more total uncompressed bytes, than the limits below.
A host source need not list sizes, so a Reader MUST also stop, with T003,
once the bytes it has read pass the byte limit.
Archives. A .moca archive is a Zip file. A Reader MUST read it as
follows:
- Entry names. Names are UTF-8. A name with a byte outside ASCII and
without the UTF-8 flag (general purpose bit 11) is rejected (
T003): a Reader never guesses another encoding. A name that is absolute, starts with a drive prefix (C:), uses\, contains NUL, or has an empty,.or..segment is rejected (T003). A trailing/marks a directory, which is ignored. - Duplicates and links. Two entries with one name collide
(
P003_PATH_COLLISION). An entry whose Unix mode (in the external attributes) is a symbolic link, or anything but a regular file, is unsupported (P001_UNSUPPORTED_ENTRY). - Compression. A Reader MUST read entries that are stored or deflated, and
MUST NOT read any other method: such an entry is unreadable
(
P002_UNREADABLE_ENTRY), so every Reader reaches the same conclusion. - Sizes. An entry is never inflated past its declared uncompressed size.
Every entry, stored or deflated, MUST be exactly its declared size and
match its CRC-32; one that is not is unreadable (
P002). The byte limit is checked against declared sizes before reading. - Directory. The central directory is authoritative.
When an entry cannot be read, the Reader stops with P002: it does not go on
with missing bytes.
Directories. A Reader MUST NOT follow a symbolic link in a directory
source, including one that replaces a file after it was listed: the file is
then unreadable (P002).
Limits. Package content is untrusted input, so a Reader MUST also bound the work it does while reading. The outcome in the last column is normative; the values are implementation-defined, MUST be documented, and SHOULD be configurable by the host under the option names given. A Reader’s defaults MUST be at least those below, which the conformance corpus assumes:
| Limit | Option | Default | When exceeded |
|---|---|---|---|
| Entries in a source | maxEntries |
20,000 | T003 |
| Uncompressed bytes in a source | maxBytes |
512 MiB | T003 |
| Frontmatter block size | maxFrontmatterBytes |
64 KiB | C002 for that node (C004 for a reserved file) |
| YAML aliases in frontmatter | — | 0 (aliases are refused) | C002 for that node |
| Structure file size | maxStructureBytes |
4 MiB | O001 |
| Triples in a structure file | maxTriples |
100,000 | O001 |
| Evidence entries checked per node representation | maxEvidencePerNode |
256 | Further entries are not checked; matched is absent |
| Source file size checked for evidence | maxSourceBytesChecked |
8 MiB | Not checked; matched is absent |
| Evidence checks per package | maxEvidenceChecksPerPackage |
10,000 | Further entries are not checked; matched is absent |
| Source bytes scanned for evidence per package, counted once per check | maxEvidenceBytesPerPackage |
256 MiB | Further entries are not checked; matched is absent |
| Depth of members (package spec §7.1.1) | maxMemberDepth |
8 | R001 for that member; it is not resolved |
| Members resolved in one read, at every depth | maxMembers |
256 | R001 for each further member; it is not resolved |
A Reader MUST normalise each evidence source once per package, not once per check, and MUST NOT let structure traversal depend on recursion depth: a long chain of relations must not exhaust the call stack.
A Reader MUST NOT request a path from a host source that the source did not list.
A Reader MUST report “target does not exist” (T001) separately from “target
has no moca.json” (T002).
4. What a Reader must never do
Section titled “4. What a Reader must never do”These apply to both classes.
- No execution. A Reader or Producer MUST NOT execute anything from a package: no evaluation, dynamic import or subprocess for any content.
- No acting on package-supplied locations or settings. A Reader MUST perform I/O only through things the host supplied and enabled: a package source, a member resolver, online signature checks, and search backends. Nothing in a package can cause a Reader to contact anything. A Reader MUST NOT use an address, a model name or a credential that a package supplies.
- No access outside the package. A Reader MUST NOT read or write outside the package while resolving package-relative paths.
- No required infrastructure. A Reader MUST NOT require a model, a vector database or an agent framework to read a package.
5. Reading a package
Section titled “5. Reading a package”A Reader MUST:
- list the package’s entries and apply package spec §6.2-§6.3: skip hidden entries, and fail closed on links, unreadable entries and path collisions;
- parse
moca.jsonand validate it againstmanifest.schema.json; - compute the digest (package spec §6.4);
- read
content/as an OKF bundle (package spec §5), group locale representations into nodes, and check paths and links; - verify attestations against the host’s trust root (§6);
- resolve members through the host’s resolver, if one is supplied, and check each against its pin (§7.1 of the package spec);
- derive capabilities (package spec §11).
Frontmatter MUST be parsed with the YAML 1.2 core schema, so that timestamps stay strings.
A Reader MUST report every problem in a package as a diagnostic, never as an exception or a crash. Malformed content is a finding; exceptions are only for programming errors, such as a missing argument.
6. Attestations
Section titled “6. Attestations”A Reader that verifies attestations MUST follow moca-attestations.md, which is the home of these rules. In summary:
- with no trust root, every attestation is unverifiable
(
A003), never valid; - a key or identity is trusted only for the roles the trust root gives it;
- offline verification is the default; online checks are opt-in, and an
online check that cannot complete is indeterminate (
A004) unless the host allowed an offline fallback; - a review whose subject digest no longer matches the file is outdated
(
A006) and does not count.
7. Citation records
Section titled “7. Citation records”Whatever a Reader hands to an application for a node, or a chunk of a node,
MUST be expressible as a citation record valid against
citation-record.schema.json:
{ "package": { "id": "https://example.com/moca/support-kb", "version": "4.2.0", "digest": "sha256:4992…", "signed": true, "signers": ["MOCA examples publisher (INSECURE, published private key)"] }, "node": { "id": "https://example.com/moca/support-kb#refund-window.md", "ref": "https://example.com/moca/support-kb@4.2.0#refund-window.md", "path": "refund-window.md", "locale": "en", "type": "Policy", "title": "Refund eligibility window", "digest": "sha256:…" }, "span": { "start": 753, "end": 1077 }, "text": "# Refund eligibility window …", "trust": { "status": "stable", "generated": { "by": "acme-drafter/2.1", "at": "2026-07-30T09:00:00Z" }, "declaredVerified": [{ "by": "human:sam.ortiz", "at": "2026-08-14T00:00:00Z" }], "attestedReviews": [{ "reviewer": "human:sam.ortiz", "reviewedAt": "2026-08-14T00:00:00Z", "outcome": "accurate", "signer": "MOCA examples reviewer (INSECURE, published private key)", "packageVerified": true }], "reviewStatus": "approved", "staleAfter": "2027-02-14T00:00:00Z", "stale": false, "inForce": true, "superseded": false, "contested": false }, "evidence": [{ "source": { "id": "terms-7", "resource": "../sources/refund-policy-2026.txt" }, "selector": { "type": "TextQuoteSelector", "exact": "Customers may request …" }, "matched": true }], "audience": "public"}Rules (members the example node does not have, such as concepts, retrieval
and profiles, are left out, as they are in a real record):
-
node.refis the versioned node reference,<package id>@<version>#<path>.node.idis the same across versions of a package, so applications SHOULD usenode.ref(ornode.idwithpackage.digest) as the key when they record what an answer cited. A packageidandversionare names a package gives itself:node.refis unique only while publishers never reuse a version, and a forged copy can carry the samenode.ref.node.idwithpackage.digestis the key that cannot be forged. A library MUST NOT hold two packages with the sameidandversionbut different digests: it keeps the first and refuses the second (L001_IDENTITY_CONFLICT). Two representations of a node sharenode.ref; when an application records a citation it SHOULD keepnode.localewith it, and any de-duplication is bynode.ref,node.localeandspantogether. -
spanoffsets are UTF-8 byte offsets into the file of the representation the record cites (<name>.<tag>.mdfor a translation). -
Every field is derived as follows, so every Reader writes the same record:
Field Value node.localeThe representation’s tag; for the default representation, the manifest’s language, or absent when it has none.node.digestsha256:and the SHA-256 of the representation file’s bytes.textWhen included, the representation file’s UTF-8 text after the closing frontmatter delimiter, or the text at spanfor a chunk.trust.statusOKF statuswhen it isdraft,stableordeprecated; otherwisestable.trust.generated,trust.declaredVerifiedOKF generatedandverified; an entry with nobyis left out.trust.validFrom,trust.validUntilThe node’s effective window (package spec §4.4), as written. trust.contestedByThe node’s moca.contested_by, as written, then<id>@<version>of each loaded package it conflicts with.audienceAs in §8; audienceInvalidistruewhen the label is malformed.profilesThe manifest’s profile URIs, in manifest order; absent when there are none. A consumer of citation records (a store, an application) SHOULD ignore properties it does not know: later versions add fields. An AG-UI client MUST (AG-UI citations §4).
-
declaredVerifiedcopies OKFverified;attestedReviewslists only reviews that verified against the trust root and match the current file, the latest from each signer, each withpackageVerified(whether the package is signed).reviewStatusisapproved,disputed,unverifiedornone(attestations §4). An application SHOULD NOT present a declared verification as an attested one, or anunverifiedordisputedreview as an approval. -
staleis true when now ≥stale_after.inForceis false outside the validity window (package spec §4.4). The host supplies the clock. -
supersededandcontestedreflect the relations among the packages the Reader has loaded, and the node’smoca.contested_by(package spec §7.2). -
evidence[].matchedistruewhen the Reader checked the selector against the cited file inside the package and it matched, andfalsewhen it checked and it did not match (C011). It is absent when the Reader did not check: the source is outside the package, its file type is not in the table below, or the Reader does not check that kind of selector. It says the quote is in the file; it does not say the node’s claim is true. -
Text is normalised before matching, as W3C Web Annotation requires (“the text MUST be normalized”). By source type, from the file name’s extension, compared case-insensitively in ASCII:
Source Normalised text .txt,.textThe file’s text, unchanged. .md,.markdown1. becomesaltand[text](url)becomestext; 2. on each line, a leading heading marker (1-6#and whitespace) and a leading list marker (-,*,+, or digits and.or), then whitespace) are removed; 3. every*,_and backtick is removed..html,.htmscriptandstyleelements removed, all tags removed, then character references decoded: numeric ones, and&<>"' (U+00A0)..vtt(WebVTT)The payload lines of every cue, cue tags removed and character references decoded as for HTML, joined with LF. The header, NOTEand other blocks without a cue timing line are skipped.For Markdown and HTML sources, the quote’s
exact,prefixandsuffixare normalised the same way, so markup in either never decides a match. ATextQuoteSelectormatches when one occurrence ofexactin the normalised text is immediately preceded byprefix(when given) and immediately followed bysuffix(when given): that is, whenprefix+exact+suffixoccurs. No whitespace or case folding is applied. A leading byte order mark is not part of a source’s text, and the Markdown markers in step 2 are removed with any white space before them. ATextPositionSelectoron its own is not checked, andmatchedis absent: offsets in range prove nothing about what is there. -
Media time ranges. A
FragmentSelectorthatconformsTohttp://www.w3.org/TR/media-frags/with a temporal valuet=start[,end](seconds, orhh:mm:sswith an optionalnpt:prefix; an empty start means 0, as Media Fragments 1.0 allows) matches a WebVTT source when the range overlaps at least one cue. Against other media it is not checked. Carry the captions or transcript of a video in the package to make its citations checkable. -
conceptslists the absolute concept IRIs the node is bound to by itsmoca.concepts(package spec §5.6).
Framework adapters (for LlamaIndex, LangChain, Microsoft.Extensions.DataIngestion and others) SHOULD store these fields as chunk metadata at ingest time, and SHOULD store the package digest on every record, so that a changed package can be detected and only changed files re-ingested.
8. Default retrieval policy
Section titled “8. Default retrieval policy”Any search built on a Reader MUST, by default, leave out records that are:
- not in force (
trust.inForceis false); - superseded (
trust.supersededis true); - deprecated (
trust.statusisdeprecated).
The host MAY opt in to including them; they are then returned with those flags set. Stale and contested content is returned by default, flagged.
When the host supplies a set of allowed audiences, a Reader MUST remove before results are returned every record:
- whose
audienceis not in the set; - whose node has a malformed
moca.audience(anything but a non-empty string; the record carriesaudienceInvalid: true), even when the host allows unlabelled content; - that has no
audience, unless the host has also allowed unlabelled content (allowUnlabelled, off by default).
Labels are compared exactly. A caller of the search MUST NOT be able to widen
the host’s audience set, allow unlabelled content, or opt in to excluded
content. A record’s audience is its representation’s moca.audience, or,
for a translation with none of its own, the default representation’s
(package spec §5.4).
Audience is a label a host filters on, not access control (package spec §13.3): fail-closed filtering protects against a forgotten label, not against someone who has the package file.
This policy applies to every search backend, including a host’s own vector store, and is applied after the backend returns (§9.2).
9. Search
Section titled “9. Search”A Reader offers one search operation. It has the same signature in every language, and the default retrieval policy is applied inside it:
search(query, { limit, includeAll, locale, concepts, scope }) -> citation records, each with a scoreconcepts keeps records bound to at least one of the given concept IRIs;
scope keeps records bound to the concept or any of its descendants by
hierarchy or parts (§10). Both refer to the package structure
(package spec §5.6).
9.1 Backends
Section titled “9.1 Backends”Search runs over a backend the host chooses and configures; a package never
chooses one (§4). A backend takes a query, a limit and optional filters
(§9.5), and returns hits valid against
search-hit.schema.json:
{ "digest": "sha256:4992…", "node": "https://example.com/moca/support-kb#refund-window.md", "path": "refund-window.md", "locale": "en", "span": { "start": 753, "end": 1077 }, "score": 3.1452 }digest is the package digest the hit was indexed from; span, when present,
is UTF-8 byte offsets into the node file (§7). A higher score is a better
match; scores from different backends are not comparable. A backend declares
lexical, dense or both (a hybrid backend, §9.6), and filterPushdown when
it applies the filters itself.
Every Reader MUST provide a lexical backend over the loaded packages’ own text, so that search works offline with nothing else installed. Dense and store backends are optional.
The built-in lexical backend searches each node representation’s title
and body, or a bound sidecar’s chunks, always reading the package’s own bytes
and never a sidecar’s text. It MUST tokenise query and text alike:
NFKC-normalise, then lower-case as package spec §6.3 does; letters, digits and
combining marks (\p{L}, \p{N}, \p{M}) form words and anything else
separates them; text in scripts written without spaces (Thai, Lao, Myanmar,
Khmer, Japanese kana, CJK ideographs) is split into single characters, each
with its combining marks, and indexed as each character and each pair of
neighbours. A representation or chunk matches when it shares at least one
token with the query. There is no stemming. How matches are scored is the
backend’s own (the reference Readers use BM25 with k1 = 1.2 and b = 0.75); a
host that needs other matching supplies its own backend.
9.2 The pipeline and the gate
Section titled “9.2 The pipeline and the gate”Each search runs these steps, in order:
- ingress (optional host hook): may rewrite the query and narrow the concepts or scope the backend searches;
- backend: returns hits;
- resolve: each hit becomes a citation record (§7), from the loaded package or, for a store backend, from the record written at ingest (§9.4); a hit that does not resolve is dropped;
- egress (optional host hook): may reorder candidates, or add records it
found through structure operations (§10), each tagged
retrieval: { via, from }; - the gate, which MUST run last, on every candidate, whatever the backend
and the hooks did:
- drop records whose package digest is not loaded (or, for a store search, not in the host’s allowlist);
- apply the default retrieval policy (§8), unless the host opted in to including excluded content;
- remove records outside the host’s audience set;
- when the caller named a locale, keep only the representation §5 of the package spec selects for it;
- when the caller named concepts or a scope, keep only records bound to them. Concepts and scope set by an ingress hook narrow the backend’s search but never bind the gate: the gate follows the caller;
- keep one record per node representation and span: two candidates with
the same
node.ref,node.localeandspan.startare one result, the first; - return at most
limitrecords.limitdefaults to 5.
Hooks are host code, supplied when the host creates the search. A package can
never supply or select one. A backend without filterPushdown can return hits
the gate removes; a Reader SHOULD ask such a backend for more hits than the
caller’s limit.
9.3 Dense backends
Section titled “9.3 Dense backends”A dense backend (semantic search) embeds the query with an embedder the host supplies. The
embedder declares a model name, an optional version and its vector
dimensions. Before a dense search over a sidecar or store, the backend MUST
compare the embedder with the index’s model. They match when all of these
hold: the names are equal; the versions are equal, if both give one; the
dimensions are equal (the index’s model.dimensions, or the length of its
vectors when that is not given); and queryPrefix and documentPrefix are
each equal, where a prefix that only one side declares is a difference:
vectors made with a prefix and queries made without one do not compare. On any
difference it MUST report S006_MODEL_MISMATCH and MUST NOT perform a dense
search. It MAY fall back to lexical search over the same text.
A Reader embeds a search query with the role query, and a store backend
embeds content at ingest with the role document
(Reader interface §5.3). The embedder
applies its model’s convention for each role. A Reader MUST NOT add a prefix
it reads from an index to what it embeds: an index is untrusted input, and
some models take free-text instructions.
A dense backend MUST report S007_NO_VECTORS for each loaded package it has
no vectors for (no sidecar, an unusable sidecar, or one without vectors), and
MUST NOT return hits from it. With S006, every loaded package a dense
backend does not search is reported, so a host never mistakes results from
some packages for results from all of them.
A dense backend scores each vector against the query by the index’s
model.distance: cosine (the default when it is absent) is cosine
similarity, dot is the dot product, and euclidean is the negative
Euclidean distance, so a higher score is always a better match.
A package can never name the embedder a Reader uses (§4); the sidecar’s
model is only compared and read for its distance, never acted on.
9.4 Store backends
Section titled “9.4 Store backends”A store backend searches content a host has already ingested into its own vector store or database. Ingest MUST go through a Reader, and every stored record MUST carry its whole citation record (§7), including the package digest. A stored record is a snapshot: whatever depends on other packages or on the trust root may have changed since ingest.
So a store search takes the host’s package states: for each loaded
package, its digest, id, version, default language, relations, relation
scopes (§7.2 of the package spec), signers, current reviews and node-level
moca.contested_by. A Reader produces a package’s state from the package
result (packageState); it is small, plain JSON that a host keeps beside its
store. At query time the Reader resolves store hits from their records and
MUST recompute from the states and the clock: stale and inForce;
superseded, supersededBy, contested and contestedBy from the relations
among the loaded packages; package.signed and signers; and
attestedReviews and reviewStatus. It keeps only records whose package is
among the states. The host therefore never needs every package in memory to
search its store, and a package superseded, or a key removed from the trust
root, after ingest is reflected at once. Records left behind by an older
version of a package are never returned, because its state is not supplied.
The store itself is trusted: the Reader cannot check a stored record’s text or audience against the package without loading it. A host whose store others can write to SHOULD NOT use it as a store backend: the Reader cannot protect it.
9.5 Filter pushdown
Section titled “9.5 Filter pushdown”When a backend declares filterPushdown, the Reader SHOULD pass these filters
to it:
| Filter | Value |
|---|---|
digests |
The digests of the loaded packages. |
exclude |
Pairs of package digest and node id that §8 excludes, unless the host opted in to including them. A node id alone is not enough: two versions of a package share node ids. |
audiences |
The host’s audience set, if any. |
locale |
The caller’s locale, if any. |
concepts |
The concepts to search: the caller’s, a scope and its descendants, or those an ingress hook chose. |
Pushdown is an optimisation. The gate (§9.2) still runs on every candidate.
9.6 Hybrid backends
Section titled “9.6 Hybrid backends”A Reader MAY offer a hybrid backend. It runs a lexical and a dense backend the
host supplies, with the same query, limit and filters, and merges their hits.
Two hits are the same hit when their digest, node, locale and span are equal;
a hit without a span stands for the whole representation and is the same hit
as no other. The host sets any weights; a package or a sidecar never does. A
package the dense backend cannot search (S006, S007) is still found
through the lexical side. The gate (§9.2) runs after the hybrid backend, as
for every backend.
How the hits are merged and ranked is not part of conformance (§14). The reference Readers use Reciprocal Rank Fusion (informative):
score(hit) = Σ over the two backends of weight_b / (k + rank_b(hit))rank_b is the hit’s 1-based position in backend b’s results; a hit a backend
did not return adds nothing from it. k defaults to 60 and each weight to 1.
Weights are at least 0, and at least one is above 0; a Reader MUST refuse other
settings as a host error, before any search.
A hit whose fused score is 0 is dropped, so a backend with weight 0
contributes nothing. Ties are ordered by node id, then locale, then span start,
then digest; a record with no span comes before one with a span. The hybrid backend declares lexical and dense,
filterPushdown when both backends do, and reports both backends’
diagnostics. Our three Readers rank identically, which their parity tests
check; another Reader may rank differently and still conform.
10. Structure
Section titled “10. Structure”A Reader answers these operations over the merged structure of every loaded package and every overlay the host supplied. They are facts, not strategy: every Reader MUST answer them identically, and the corpus tests them exactly. Terms and meanings are in package spec §5.6.
| Operation | Returns |
|---|---|
concept(iri) |
The concept’s preferred label (no language tag, then en, then the first language tag in order, §2 of the package spec), labels, definition, deprecated, what it replaces and what replaces it, and its layer; or nothing when no layer declares it. |
requires(iri, { transitive }) |
The concepts iri requires. Direct: ordered by IRI. Transitive: every concept reachable through requires, each after the concepts it requires (depth first, ties by IRI). |
requiredBy(iri) |
The concepts that require iri, ordered by IRI. |
parts(iri) |
The direct parts of iri, ordered by IRI. |
narrower(iri, { transitive }) |
Narrower concepts; transitively, breadth first, each level ordered by IRI. |
broader(iri), related(iri) |
Direct broader concepts; related concepts in either direction. Ordered by IRI. |
sequence(iri) |
The members of the ordered collection iri, in list order; nothing if iri is not an ordered collection. |
nodes(iri, { include }) |
Citation records of the nodes bound to iri, and, with include of narrower, parts or both, to its descendants, with the default retrieval policy applied, ordered by node.ref. |
Every concept item carries its iri, its preferred label (absent when the
concept has none) and the layer that stated the fact: package,
application or organisation.
The concept result. concept(iri) returns an object with these members,
in this order:
| Member | Value |
|---|---|
iri |
The concept’s IRI. |
label |
The preferred label; absent when there is none. |
labels |
Every skos:prefLabel, keyed by language tag ("" for none). |
definition |
The preferred skos:definition, chosen as the label is; absent when there is none. |
deprecated |
true when a layer states owl:deprecated true, otherwise false. |
replacedBy |
The concepts that replace this one, ordered by IRI. |
replaces |
The concepts this one replaces, ordered by IRI. |
layer |
The layer that declared the concept. |
Cycles. O005 is a warning, so a merged graph can still hold cycles, and
overlays can add them. Every traversal keeps a set of visited concepts: each
concept appears at most once, the starting concept never appears in its own
result, and a concept already visited is skipped. For transitive requires,
a concept still on the current path is skipped too, so inside a cycle a
concept can come before one it requires. narrower lists each concept at the
shallowest level it is reached.
Layers. The host MAY supply overlays: Turtle it loads over the packages’
structure, as the application layer or the organisation layer.
Overlays are parsed with the same rules and limits as structure.ttl; a
problem in one is reported with the overlay as the file. An overlay can only
add: when two layers state the same fact, it is reported with the first layer
that states it, in this order: package, application, organisation. Overlays are never
covered by a package’s digest or signatures, and a package can never name or
load one.
Describes, never enforces. requires reports what the structure says.
Whether a person may see content depends on runtime state that only the
application holds. In the same way, deprecated, replaces and replacedBy
are reported, never acted on: structure never removes a record from search or
from nodes() (package spec §5.5).
Overlays never change a package. A package’s capabilities, validity and diagnostics come from the package alone; the structure operations are the only place overlays appear.
11. Diagnostics
Section titled “11. Diagnostics”A Reader MUST report findings as structured values with code, severity
(error, warning or info), message, and, where it applies, file
(package-relative) and line (1-based). Codes are the contract; message text
is not, and hosts MUST NOT match on it. A Reader MUST offer a strict mode that
promotes warnings to errors without changing codes. Validity then follows
the promoted severities: a T, P, M or C warning makes the package
invalid, and a promoted warning in another family affects only its feature.
A package is valid when it has no error in the T, P, M or C
families. Errors in other families affect only the feature concerned: an
invalid attestation does not count, a
mismatched member is not used, an unusable sidecar is ignored. O diagnostics
withhold only the structured capability.
| Code | Severity | Meaning |
|---|---|---|
T001_TARGET_NOT_FOUND |
error | The target does not exist. |
T002_MANIFEST_NOT_FOUND |
error | The target exists but has no moca.json at its root. |
T003_ARCHIVE_REJECTED |
error | An archive or host source broke a safety rule or limit. |
P001_UNSUPPORTED_ENTRY |
error | The package contains a symbolic link or other non-regular file. |
P002_UNREADABLE_ENTRY |
error | A package entry could not be read. |
P003_PATH_COLLISION |
error | Two package paths are equal after Unicode NFC normalisation, or after normalisation and simple lowercase mapping (package spec §6.3). |
M001_MANIFEST_INVALID_JSON |
error | moca.json is not valid UTF-8 JSON. |
M002_MANIFEST_SCHEMA |
error | moca.json does not validate against the manifest schema. |
M003_NO_CONTENT |
error | The package has neither a content node nor a member. |
M004_UNKNOWN_KEY |
info | A top-level manifest key is neither defined by the spec nor a vendor x- |
M005_DUPLICATE_MEMBER |
error | The same member id appears more than once. |
M006_VALIDITY_WINDOW_INVALID |
warning | validUntil is not later than validFrom. |
M007_UNREGISTERED_URN |
info | The package id uses a URN namespace that is not registered with IANA. |
M008_DIGEST_V1_PACKAGE |
info | The package was written for a mocaVersion before 0.4; digests, pins and attestations made then will not match moca-digest-v2 (package spec §14). |
M009_MOCA_VERSION_NEWER |
warning | The package was written for a newer mocaVersion than this Reader implements; it is read under the newest rules this Reader implements (package spec §14). |
C001_FRONTMATTER_MISSING |
error | A concept document has no YAML frontmatter (OKF MUST). |
C002_FRONTMATTER_INVALID |
error | Frontmatter is not parseable YAML or is not a mapping. |
C003_TYPE_MISSING |
error | Frontmatter has no non-empty type (OKF MUST). |
C004_RESERVED_FILE_INVALID |
error | index.md or log.md breaks the OKF reserved-file rules. |
C005_FIELD_INVALID |
warning | A frontmatter field does not match the node schema. |
C006_EVIDENCE_SOURCE_UNKNOWN |
warning | moca.evidence names a source id that sources[] does not declare. |
C007_LINK_UNRESOLVED |
warning | An internal Markdown link does not resolve. |
C008_UNSAFE_PATH |
error | A package-relative path is absolute or escapes the package root. |
C009_LOCALE_ORPHAN |
warning | A locale representation has no default-language file. |
C010_VALIDITY_WINDOW_INVALID |
warning | moca.valid_until is not later than moca.valid_from. |
C011_EVIDENCE_SELECTOR_UNMATCHED |
warning | An evidence selector does not match the text source file it cites inside the package. |
A001_ATTESTATION_MALFORMED |
error | An attestation file is not a parseable envelope and statement. |
A002_ATTESTATION_INVALID |
error | An attestation failed verification or does not match the package. |
A003_ATTESTATION_UNVERIFIABLE |
warning | An attestation could not be checked (attestations §7 says when). |
A004_ATTESTATION_INDETERMINATE |
warning | Online verification was requested but could not complete. |
A006_REVIEW_OUTDATED |
warning | A review attestation covers a file whose bytes have since changed or been removed. |
A007_REVIEW_DATE_FUTURE |
warning | A review is dated later than the host clock, so it does not count (attestations §4). |
A008_PACKAGE_DIGEST_MISMATCH |
error | A package attestation names this package’s id and version with a different digest: the content changed after it was signed (attestations §3). |
A009_SIGNATURE_REQUIRED |
error | The host requires a valid package attestation (requireSignature) and the package has none. This one makes the package invalid. |
R001_MEMBER_UNRESOLVED |
warning | A member could not be resolved by the host resolver. |
R002_MEMBER_DIGEST_MISMATCH |
error | A resolved member does not match its pinned digest. |
R003_MEMBER_CYCLE |
error | Members form a cycle. |
R004_RELATION_UNAUTHORISED |
info | A supersedes or conflictsWith relation is not authorised by a signer of the package, so it has no effect (package spec §7.2). |
L001_IDENTITY_CONFLICT |
error | A library was given a second package with the same id and version but a different digest; the second is refused (§7). Reported by the library, not the package. |
F001_PROFILE_UNRECOGNISED |
info | A declared profile is not recognised by this Reader. |
O001_ONTOLOGY_UNPARSEABLE |
warning | structure.ttl or a host overlay is over the size or triple limit (§3), or is not parseable Turtle. |
O002_CONCEPT_UNDECLARED |
warning | A relation, ordered collection or node binding uses a concept that is not declared as a skos:Concept. |
O003_REMOTE_REFERENCE |
warning | structure.ttl or a node binding relies on something a Reader would have to fetch or resolve: owl:imports, or an IRI that is not absolute. |
O005_STRUCTURE_CYCLE |
warning | requires, broader or hasPart forms a cycle (package spec §5.6). |
S001_SIDECAR_INVALID |
error | index.json does not validate against the sidecar schema. |
S002_SIDECAR_TARGET_MISMATCH |
error | The sidecar is bound to a different package id or version. |
S003_SIDECAR_STALE |
warning | The sidecar digest does not match the package; the sidecar is ignored. |
S004_SIDECAR_ITEM_INVALID |
error | A payload item breaks the addressing rules. |
S005_SIDECAR_FORMAT_UNKNOWN |
info | The payload format is not recognised; the sidecar is ignored. |
S006_MODEL_MISMATCH |
warning | The host’s embedder does not match the index’s model; dense search is refused. |
S007_NO_VECTORS |
warning | A loaded package has no vectors a dense backend can search; that package is not searched. |
Retired codes, never reused: A005_SKILLS_WITHHELD, K001_SKILL_INVALID, O004_STRUCTURE_VIEW_MISMATCH.
12. Handing content to a model
Section titled “12. Handing content to a model”Package text is untrusted input (package spec §13.1).
Package-supplied text is every string that comes from a package: node
bodies, titles, descriptions, tags, every frontmatter value (x- keys
included), manifest fields, structure labels and definitions, evidence
quotes, sidecar text, file paths, and names in attestations. All of it is
untrusted, not only the body.
A Reader, adapter or server that passes content to a model:
- MUST pass package-supplied text as data inside an envelope, never as system or developer instructions;
- MUST escape package-supplied text so that it cannot end the envelope or start a new part of it. A JSON document, with the text in JSON strings, is the RECOMMENDED envelope: JSON’s own escaping does this. Any other delimiter (a tag, a fence, a marker line) MUST be escaped or removed wherever it appears in the text;
- MUST NOT grant tools, change settings or follow links because content asks;
- SHOULD tell the model, outside the envelope, that retrieved text is reference material to quote and cite, not instructions.
Rendering. Applications are not a conformance class, so this is advice (package spec §1.1); an AG-UI client is bound by it (AG-UI citations §4). An application that shows package-supplied text, or a model’s answer built from it, to a person:
- SHOULD make links only for
httpandhttpsURIs, and show any other scheme (javascript:,data:,file:and the rest) as plain text; - SHOULD NOT load remote resources named in package text automatically: no images, frames, styles or scripts fetched because the text names them. A Markdown image in a package or an answer can send data to whoever controls its URL;
- SHOULD render package text as text or as sanitised markup, never as active content.
13. Degradation
Section titled “13. Degradation”| Condition | Required behaviour |
|---|---|
| Unknown top-level manifest key | Ignored, never acted on; M004 (info). |
| Unknown profile | Package read as core; profile data preserved; F001 (info). |
Unknown type, missing optional field |
Accepted (OKF rule). |
| Malformed optional frontmatter field | C005 (warning); node still read. |
| Unknown relation type | Preserved; no effect. |
| Unknown attestation predicate | Ignored. |
| No trust root | Attestations unverifiable; package still readable. |
| No resolver | Members exposed unresolved; no diagnostic. |
| Unresolvable or mismatched member | That member is not used; the rest usable. |
| No sidecar, stale sidecar, or unknown payload format | Sidecar ignored; the package searched directly. |
| No embedder, or an embedder that does not match the index model | No dense search (S006 on mismatch); lexical search still available. |
| A package without vectors, under a dense backend | That package is not searched by the dense backend (S007); lexical search still available. |
| Evidence source that cannot be checked | evidence[].matched absent; no diagnostic. |
| Structure or binding problem | O warning; structured capability withheld; the rest usable. |
14. Demonstrating conformance
Section titled “14. Demonstrating conformance”An implementation claiming conformance MUST name its class, the contract
version, and any optional features it does not implement (archives, host
sources, Sigstore verification, sidecars, profiles). “Optional features” are
what an implementation does; “capabilities” are what a package has
(package spec §11). It MUST pass every
case in conformance/cases.json except those
that need an optional feature it names, producing the expected validity, diagnostic codes, capabilities and
digests. A Reader MUST also pass every search case, which checks what is
returned and what is never returned but never the ranking, and every
structure case, which checks the answers to structure operations exactly.
Message text is never compared.
An implementation in another repository runs the corpus through the runner protocol in conformance/README.md.