spec 1.0 · normative

The Machine-Readable Surface

Five artifacts make the context layer legible to tooling. Everything else in the context layer is prose for humans that agents happen to read; these five are the contract tools build against.

Artifact Default location Schema
Manifest leji.json (repository root, fixed) context-manifest.schema.json
Context index <root>/context-index.json context-index.schema.json
Context changelog <root>/context-changelog.json context-changelog.schema.json
Agent profiles <root>/agents/*.md (frontmatter) agent-profile.schema.json
Decision records <root>/decisions/*.md (frontmatter) decision-record.schema.json

All locations except the manifest are manifest-declared; the table shows defaults.

Requirements#

  1. Manifest. leji.json MUST exist at the repository root and validate against its schema. It is the only fixed filename in Leji: the file tooling reliably looks for.
  2. Index. A context layer claiming indexed conformance or above MUST carry a context index that is generated, never hand-maintained: tooling resolves the category index files (categories.<id>.indexes, per content-categories.md) to the documents they list and writes one entry per governed document. Each entry carries a stable id, a path, a title, and a category identifier. A generator SHOULD also emit the document’s kind (intent or record); it is optional in the schema so an index written before kinds existed stays valid, and a consumer treats an absent value as intent. A record’s entry additionally carries its date when the document declares a valid frontmatter date; the generator sources dates from frontmatter only, never from prose or filename conventions. A stale index (one that no longer matches what the index files resolve to) MUST be treated as a validation failure. A host that declares federation.mounts also carries, in the same index, a top-level mounts array: one routing record per mount (name, source, pin, trackingRef when declared, owner, role when declared, and the routing metadata categories / topics / requiredWhen). These are routing records only; tooling MUST NOT copy a sibling’s entries or prose into the host index, and a mount record carries nothing the host’s audience may not see (per distribution.md, Restricted mounts).
  3. Changelog. A context layer claiming indexed conformance or above MUST carry a machine-readable changelog of context layer changes. Entries carry a stable id, a UTC date, a type, a one-line summary, and the affected paths. Canonical order is derived, not positional: tooling MUST order entries by (date, id) ascending, and array position carries no meaning. Because id is unique within the changelog (Identifiers), (date, id) is a total order even when two changes share a date. Surviving entries are immutable: tooling MUST treat modification of a published entry as a validation failure wherever it can establish the prior state, and reordering the array is not a modification. Establishing that state needs a distinct baseline to compare against; where the reference tooling has only the current revision, as in an ordinary continuous-integration checkout, the modification is not visible to it and the review of the change set is what catches it (see conformance.md). The changelog is a recency surface, not an archive: a long-lived context layer SHOULD compact it rather than let it grow without bound, and MAY compact it at any time by removing entries from the oldest end of that order, provided the same change set appends an entry of type compaction whose compacted field records the count and the first and last removed ids. Removal of anything but the oldest entries, removal without a compaction entry, and compaction to an empty file are validation failures. Append-only discipline is set-keyed by id and checked against the prior committed state, so it needs git at authoring time; the file itself stays git-free for consumers, and git history holds the full record. A change set that touches governed documents (those the category index files resolve to) MUST append an entry whose paths cover the governed paths it changed, so every changed governed document falls under some appended entry: append-only discipline keeps published entries immutable, and this coverage rule keeps the record complete. A human-readable changelog MAY exist alongside; the JSON record is the one tooling reads.
  4. Frontmatter artifacts. Agent profiles and decision records are markdown documents whose YAML frontmatter validates against their schemas. The prose body stays free-form; the frontmatter is the machine contract. Pure-JSON profiles or decisions MUST NOT be required: people read these documents.
  5. Identifiers. All id values MUST be stable once published: renames and moves update path, never id. Identifiers are lowercase, hyphen-separated, unique within their artifact type. A generated index entry’s id is derived in priority order: the document’s frontmatter id if it declares one; else the id the stored index already carries for that same path, or, for a pure content-preserving move, for that same content; else a slug of the filename, de-collided against its parent directory. The first that exists wins, so a published id survives a rename or a move and only a brand-new document mints a fresh one. A document that might be moved and edited in one change set SHOULD declare a frontmatter id: only frontmatter pins the id through a simultaneous path and content change (the carry-by-path and carry-by-hash fallbacks both miss it), and tooling warns (id-vanished) when a stored id disappears so the dangling references it leaves are caught.
  6. Timestamps. Changelog date values are ISO 8601 in UTC: either a calendar date YYYY-MM-DD (ordered as that day’s start, T00:00:00Z) or a whole-second timestamp ending in Z (for example 2026-06-13T15:04:05Z). Zoneless times, non-UTC offsets and fractional seconds are not permitted: fractional seconds would break the guarantee that a lexical sort of date is a chronological sort, since …05.1Z sorts before …05Z while being later. Every date field in every artifact is calendar-ranged, so a month of 13 or a day of 99 is invalid. Other artifacts’ dates follow ISO 8601 and MAY be date-only. Paths are POSIX-style, relative to the repository root, no leading ./.
  7. Every JSON artifact except the manifest MUST declare the schema line it was written against (schemaVersion), per versioning.md; the manifest declares its target spec line with the self-naming leji key.
  8. Derived surfaces inherit access constraints. The index, the changelog, the generated viewer, and any compiled or exported view built from context layer content are derived surfaces, as is the output an agent produces from that content. A derived surface carries the access constraints of the most restricted content it draws from. A derived surface MUST NOT be written or copied to a location with a broader audience than that content without an explicit, reviewed redaction step that produces a separate surface for that audience, and an agent MUST NOT quote or summarize restricted context into a broader-audience or less-restricted surface (a pull request, ticket, chat, commit message, or public context layer). The index of a restricted context layer can be as sensitive as its prose: titles, paths, and summaries all describe it. This is a constraint on the people and agents who operate the tooling, not a check the tooling performs: Leji defines no audience model a tool can read to compute “broader audience” (access is the version control system’s, per governance.md, Access boundary), so the reference SDK does not enforce it and the most any tool does is warn (the viewer export warns to host it privately).

Task routing#

The index, the category assignments, and the decision records exist so an agent can load the slice of context a task needs instead of the whole tree. This section defines, normatively, how a task’s scope selects that slice. It is the one routing algorithm the rest of the specification refers to: the boot profile’s Loading section (boot-profile.md) points an agent here in task language, decision-record scope (decisions.md) is matched by it, and federated reading (distribution.md) reuses it to decide which siblings a task touches. Routing reads context; it is not a task envelope or an execution protocol, which stay outside 1.0 (see README.md, Extension boundary).

  1. Input. A task’s scope is the set of repository-root-relative POSIX paths the task reads or changes (normalized per Requirement 6: POSIX-style, root-relative, no leading ./), together with any categories the task explicitly names and any topics the task explicitly names. Topics are explicit inputs: the algorithm never derives them from paths, categories, prose, or content. How an agent or tool derives the scope from the task is out of normative scope; the matching below is not.
  2. Path match (lexical, bidirectional). A declared path and a task path match when, after normalization (POSIX-style, root-relative, no leading ./, any trailing / removed), the two strings are equal, or one is a path-prefix ancestor of the other: the shorter equals the longer truncated at a / boundary. Matching is purely lexical: it never consults the filesystem, and it draws no distinction between a path that names a file and one that names a directory, because after normalization the two are indistinguishable. This is containment in either direction (the underPath relation the reference implementations share), so a broadly-scoped task and a narrowly-declared selector find each other whichever side is broader.
  3. Category match (narrow), and the two category sets. A task’s categories divide into expanded and signalled. A category the task explicitly names enters both sets. A task path that is itself a governed document (matching its generated index entry by exact equality, never by containment) contributes that entry’s category to the signalled set only. Expanded categories load their intent documents and record candidates; signalled categories are a matching signal for decisions and federation mounts and load nothing on their own. A category selector MUST NOT infer a category for an arbitrary repository file, and a task path that is not itself a governed document, including any ancestor directory of one, contributes no category at all. Path scope reaches files; category expansion does not follow it.
  4. Topic match (exact, mount-only). A topic is a non-empty string of Unicode scalar values, compared by its UTF-8 encoding; a lone surrogate is not a valid topic. Both sides are held to that rule: a task topic or a mount topics entry that is not a non-empty string of Unicode scalar values is an input error, and an implementation MUST reject it rather than return it as a silent non-match. A task topic matches a declared topic when the two decoded strings are exactly equal. Implementations MUST NOT case-convert, Unicode-normalize, locale-compare, trim, tokenize, substring-match, or fuzzy-match either side, so canonically equivalent spellings that differ in bytes do not match; this equality rule is separate from the bytewise ordering of results below. Duplicate task topics form one signal, so naming a topic twice matches exactly as naming it once does. A federation mount matches when any task topic equals any topic the mount declares. A topic match selects the mount only: it MUST NOT enter the expanded or signalled category sets, load any document or record, route any decision, evaluate requiredWhen, or make a mount required.
  5. Status filter. Only a decision record whose status binds is routed as current guidance. accepted and deprecated bind; a deprecated record binds with a stale posture, and an agent MUST treat it as guidance on its way out rather than settled current practice. A superseded record MUST NOT bind except as history and MUST carry supersededBy; proposed and rejected records MUST NOT bind. A record that binds is live.
  6. Unscoped decisions. A live decision record that declares neither affectedPaths nor affectedCategories is org-wide: it is routed for every task, whatever the task’s scope. Scoped live decisions are routed only when the task matches them by path (2) or category (3).
  7. Empty path scope, and empty scope. When the task’s path set is empty, path matching contributes nothing and an agent MUST state that path-scoped routing was not evaluated; explicitly named categories are still honored and still expand, and explicitly named topics are still matched. Named categories and named topics both count toward a non-empty scope. The task’s whole scope is empty only when it names no paths, no categories, and no topics; then an agent routes only the unconditional boot-profile and agent-profile context plus the org-wide unscoped live decisions. An agent MUST NOT present an unrouted load as though it were scoped.
  8. Records route as candidates. An expanded category routes its intent documents as required context; that category’s records are returned separately, each with its kind and date, as candidates the reader loads by judgment. A record becomes required only when the task’s paths select it directly under item 2, an agent or boot profile names it, or a person asks for it; being a category match, or carrying the newest date, never makes a record required. Where a record is both a category candidate and directly path-selected, the direct selection wins and it is required. Routing MUST NOT certify any record as “the latest” or “the current one”: 1.0 defines no record-series identity or ordering guarantee, so recency judgments belong to the reader, made against the dates the index surfaces. Decision records keep their own routing (items 5 and 6) and never route as generic records. Naming a decision file as a task path does not route that decision; its declared scope does.
  9. Citation. An agent that loads routed decision records MUST cite which matched records it loaded, so a reader can see which guidance the agent applied and infer what it did not.

The routed slice is what an agent loads for a task. It is the union of: the boot profile’s unconditional load set and the active agent profile’s requiredRead, which the agent holds as its baseline independent of any scope; every governed intent document in an expanded category; every governed entry the task’s paths select under item 2; every record directly path-selected under item 8; and every live decision the task matches by path or by a signalled or expanded category, plus the org-wide unscoped live decisions.

Signalled categories contribute matching only, to decisions and to federation mounts, and never expand a corpus.

The slice is not the envelope. A tool that computes routing returns the slice alongside material the agent must not load unprompted: record candidates, and the routed-candidate metadata around them. Loading the envelope wholesale defeats the point of routing.

Result ordering is normative where a tool emits one, so independent implementations agree byte for byte: categories in the canonical category order of this specification; documents, records and decisions by ascending path; mounts by ascending name. String comparison is bytewise over UTF-8, not locale- or codepoint-collation dependent.

Tooling MAY offer a helper that computes this slice from a path set; the reference implementations expose one (route). Such a helper computes the scope-dependent portion and is not required to emit the baseline, which the caller already holds; the agent’s obligation to load that baseline is unchanged. Routing conforms whenever a raw reader follows this algorithm.

Notes (non-normative)#

The reference tooling checks the changelog’s schema and append-only discipline today (leji validate runs both); verifying changelog coverage against a base revision, that every changed governed path appears in an appended entry, is a reported check on the roadmap, not yet a blocking gate. Until it ships, coverage rides the process-attested review and CI discipline (see conformance.md).

The index is the navigation source for the governed context: leji viewer renders that governed spine from the index, and below it the repository’s own directory tree as a browsable reference zone, so one view surfaces both the governed context and the team’s existing navigation. Any docs tool can project the index the same way. Presentation is non-normative. The surface is deliberately small. Five shapes are enough for tooling to validate a context layer, diff it, score its freshness, and route an agent to the right slice, and few enough that a team can hold the whole surface in mind. Anything beyond these five is post-1.0 territory, gated on lived practice.