Appearance
Family contracts
Yggdrasil, Grain, Jarl and Horde exchange machine documents — JSON or YAML with a named, versioned shape — and every one of them is listed here. One page, because a contract that each repository describes in its own README is three descriptions that drift.
The table is a register of every machine document the family writes, not only the ones that cross a repository boundary. A document nothing outside its own tool reads still gets a row, with no external consumer in the consumer column: a list that tries to hold only the cross-repository ones goes stale the week somebody adds an internal document, and nobody notices until a consumer appears for it.
A check fails when this page and the code disagree — in either direction, in Yggdrasil, Grain or Horde; Jarl guards its two contracts in its own suite. See the guard below.
The register
| Document | Schema id | Producer | Consumers | Since | Described where |
|---|---|---|---|---|---|
Graph files (.yggdrasil/model/**/yg-node.yaml, yg-architecture.yaml) | no schema id — the graph's own format | Yggdrasil, and whoever edits the graph | Grain — grain advise and grain propose over a graph that already exists read them directly | before 6.0.0 | Configuration |
Run report (yg check --json) | yg-check/1 | Yggdrasil | Horde — the rule ladder (node.mjs promote, demote, ladder) and the wave close's quality index | before 6.0.0 | CLI Reference |
Context package (yg context --json) | yg-context/1 | Yggdrasil | Horde — node resolution, land; neither reads the log fields | before 6.0.0; the optional typeDecisions (decisions in force for the type and the types above it) and nodeLog (the component's own log) fields arrived in 6.1.0 | CLI Reference |
Blast radius (yg impact --json) | yg-impact/1 | Yggdrasil | Horde — port consumers | before 6.0.0 | CLI Reference |
Component (yg node --json) | yg-node/1 | Yggdrasil | Horde — node existence and structure | before 6.0.0 | CLI Reference |
Rule list (yg aspects --json) | yg-aspects/1 | Yggdrasil | Horde — init, the rule ladder and the quality index, the legislator's brief, law, land | before 6.0.0 | CLI Reference |
Attention feed (yg advise --json) | yg-advise/1 | Yggdrasil | Horde — audit | before 6.0.0 | CLI Reference |
Rule history (yg log read --aspect --json; yg aspects log read --json before 6.1.0) | yg-aspect-log/1 | Yggdrasil | Horde — law | before 6.0.0; the command changed in 6.1.0, the document did not (entries gained supersedes and supersededBy) | CLI Reference |
Waiver inventory (yg suppressions --json) | yg-suppressions/1 | Yggdrasil | Horde — land | 6.0.0 | CLI Reference |
Drill run (yg drill --json) | yg-drill/1 | Yggdrasil | Horde — the rule ladder's drill evidence (node.mjs runDrill); Grain — grain propose, which decides whether a mined rule ships enforced, advisory or draft from its counts (propose-status.mjs runDrill; reads the text summary only from a 6.0.x CLI) | 6.1.0 | CLI Reference |
Rule health (yg aspects --health --json) — its own document, not the rule list | yg-aspects-health/1 | Yggdrasil | Horde — the wave close's audit (quiet rules) | 6.1.0 | CLI Reference |
Command error (any command run with --json that fails): code, what, why (null when there is none), next: { command, text } — command an argument vector or null, the same form yg-check/1's next.command takes | yg-error/1 | Yggdrasil | Horde — every document read (node.mjs ygJson; it reads code, what, why and next.text, never next.command) | 6.1.0 | CLI Reference |
Node list (yg tree --json) | yg-tree/1 | Yggdrasil | no external consumer | 6.1.0 | CLI Reference |
File owner (yg owner --json) | yg-owner/1 | Yggdrasil | no external consumer | 6.1.0 | CLI Reference |
File owner batch (yg owner --files --json) | yg-owner-batch/1 | Yggdrasil | no external consumer today — built for Horde's territory resolver, one node or type:<id>@<top-level dir> unit per file (447) | 6.1.0 | CLI Reference |
Search results (yg find --json) | yg-find/1 | Yggdrasil | no external consumer | 6.1.0 | CLI Reference |
Component log (yg log read --node --json) | yg-log/1 | Yggdrasil | no external consumer | 6.1.0 | CLI Reference |
Type decisions (yg log read --type --json) — a node type's decision log | yg-type-log/1 | Yggdrasil | no external consumer (Horde writes decisions with yg log add --type; it reads none back) | 6.1.0 | CLI Reference |
Marketplace manifest (yg-marketplace.yaml) | yg-marketplace/1 | the marketplace author; Yggdrasil's yg marketplace init writes it and yg pack new adds an entry | Yggdrasil — yg pack add / update / verify / new, yg marketplace check (yg pack list asks the source for tags and never reads it) | 6.0.0 | Packages |
Package manifest (yg-package.yaml) | yg-package/1 | the package author | Yggdrasil — yg pack, yg marketplace check | 6.0.0 | Packages |
Package record (.yggdrasil/yg-packages.yaml) — what is installed, not a verdict lock | yg-packages/1 | Yggdrasil — yg pack | Yggdrasil — the package-file-modified rail and yg advise; no external consumer | 6.0.0; the optional requested, tag, commit and identity fields arrived in 6.1.0, and a record without them is still read | Packages |
Refused-content record (.yggdrasil/.refused/<hash>.json, local and gitignored) | versioned by v: 1; joined to the event line and the lock entry by hash | Yggdrasil — yg check --approve, one file per refusal it records | Grain — the refusal→fix pairs a rule's own verdicts give (a reader, not yet shipped) | 6.1.0 | Reviewers |
Verification event line (.yg-events.jsonl, yg-events.llm.jsonl and its sealed months yg-events.llm.<YYYY-MM>.jsonl) | yg-events — no schema field; versioned by v: 1 | Yggdrasil — yg check --approve | Grain — tests/stress/labels.mjs verdicts, a research reader of the verdict history, not a shipped command (Horde's cost report, which read the committed stream, is gone) | before 6.0.0; the sha field arrived in 6.0.0; the committed stream is sealed by month since 6.1.0, so a reader reads the month files too | Configuration |
Advice (grain advise --json) | grain-advice/1 | Grain | Horde — queue quality, the wave close's audit, and the legislator's brief, which carries the rule items that hold in one of the territory's components as drafts (advisory at most, for the owner to ratify); Yggdrasil — yg advise import | before 6.0.0; the legislator's brief reads it since 6.1.0 | this page |
Proposed graph (proposal.json) | grain-proposal/1 | Grain — propose | Yggdrasil — yg adopt | before 6.0.0 | this page |
Measured breakage per proposed rule (aspects/<id>/provenance.json in a proposal) | no schema id — one file per rule | Grain — propose | Yggdrasil — yg adopt, which reads existingViolations for its "Already broken" count | before 6.0.0 | this page |
Package versions cache (.yggdrasil/.yg-packages-versions.json) | yg-package-versions/1 | Yggdrasil — the package commands | Yggdrasil — yg advise; no external consumer | 6.0.0 | CLI Reference |
| Repository export | grain-export/1 | Grain — grain export | adopter pipelines and audits; no external consumer inside the family | before 6.0.0 | this page |
Convention check (grain check --json, grain review --json) | grain-check/1 | Grain | no external consumer | before 6.0.0 | this page |
Obligation (grain obligation --json) | grain-obligation/1 | Grain | Horde — tk new | before 6.0.0 | this page |
Co-change partners (grain completeness --json) | grain-completeness/1 | Grain | Horde — the architect, planned (it does not read the document yet) | 6.1.0 | this page |
Co-change and cut score (grain cochange --json) | grain-cochange/1 | Grain | Horde — refine --step cut: the accepted cut is scored as a partition (grain cochange --partition <file> --level node --runs 3) before the plan's review, kept in cut-score.json and shown in the review brief; advice only, it never refuses a cut | 6.1.0 | this page |
Territory measurement (grain measure --json) | grain-measure/1 | Grain | Horde — report: the mission's territory measured from the commit its trunk was cut at to the trunk's tip, over the cut's components (grain measure --from --to --scope), taken on a wave close, at horde.mjs done and when report is run by hand, and carried in the measure field of horde-report/1 | 6.1.0 | this page |
Proposal report (grain propose --json) | grain-propose/1 | Grain | no external consumer | before 6.0.0 | this page |
Oracle record (oracle.json) | grain-oracle/1 | Grain — grain oracle record | Grain's own scoring; no external consumer | before 6.0.0 | this page |
| Adopter correction inside an oracle record | grain-correction/1 | Grain — grain oracle record | Grain's own scoring; no external consumer | before 6.0.0 | this page |
Oracle file list (files.json) | grain-oracle-files/1 | Grain — grain oracle record | Grain's own scoring; no external consumer | before 6.0.0 | this page |
Oracle scorecard (grain oracle score --json) | grain-oracle-score/1 | Grain | no external consumer | before 6.0.0 | this page |
Look-alike groups (.family-candidates.<producer>.json; the file name keeps the older word "family") | no schema id — versioned by v: 1 | two, each in its own file and naming itself in producer and its test for "without a rule" in gate: Grain — grain propose writes .family-candidates.grain.json (producer: grain, gate: no-certified-convention) into the proposal's .yggdrasil/ so yg adopt installs it with the graph (--family-candidates <path> writes it elsewhere, for a repository that adopted earlier); Yggdrasil — the offline miner scripts/family-without-law.mjs in the Yggdrasil repository writes .family-candidates.yggdrasil-miner.json (producer: yggdrasil-miner, gate: no-narrow-aspect) into .yggdrasil/. One file per producer, so one producer's run never erases the other's look-alike groups; the shared .family-candidates.json of earlier releases is still read | Yggdrasil — yg advise, which rejects any v it does not name, and names each file it could not use (an unknown v, unparseable JSON, no usable ts) on its Attention section | before 6.0.0 | this page |
Loop record library (skills/jarl/scripts/record.mjs, vendored with jarl-lib.mjs beside it) — a fixed list of exports, each with its parameters and opts keys, not a file; its version is the RECORD_API constant | jarl-record/1 | Jarl | Horde — every mission is a Jarl loop, read and written through this module, which Horde vendors with jarl-lib.mjs at a pinned Jarl commit (vendor/jarl.pin.json, whose api names this id) and refuses to run when the copy's RECORD_API differs | 6.1.0 | this page |
Loop profile (init --profile <file>, stored as .jarl/profile.json): the statuses an issue moves through and what each means, the fields, sections and acceptance heading, the external scheduler and the done gate | jarl-profile/1 — no schema field; versioned by "jarl-profile": 1 | whoever opens the loop — Horde brings its own (jarl-profile.json: its statuses, the ones only its own code may set, its scheduler, its done gate and its lifecycle) | Jarl — every command of a loop opened with it, which refuses a version it does not read | 6.1.0 | this page |
Loop views (status, resume, report, queue, tips, list and show, each with --json) — one JSON block each, fields listed in Jarl's manual | no schema id — described field by field in Jarl's manual | Jarl | dashboards and scripts outside the family; Horde reads the same data through jarl-record/1 (statusData, resumeData), never the command output | 6.1.0; report --json carries metrics, with check: whether the loop lands on a declared check or on testimony | this page |
| Law diff | horde-law/1 | Horde — law | the session | 6.0.0 | this page |
| Mission retrospective | horde-retro/1 | Horde — retro | the session; no external consumer | 6.0.0 | this page |
| Mission plan | horde-plan/1 | Horde — queue plan | Horde itself; no external consumer | before 6.0.0 | this page |
| Drill case | horde-drill-case/1 | Horde — drill | Horde itself; no external consumer | before 6.0.0 | this page |
Mission report (report.mjs --json; rendered as hordes/<h>/report.md, rewritten on tick, wave close and ask) | horde-report/1 | Horde — report | the client, through the rendered report.md; no machine consumer in the family | 6.1.0; the measure field (the grain-measure/1 reading: before, after, range) arrived with it | this page |
before 6.0.0 means the document already existed when the family started releasing together; 6.0.0 is the first joint release, so it is the first version this column can name precisely.
One number for the family
6.1.0 asks for upgrade steps under a minor number. Semantic Versioning would call it 7.0.0: the check is stricter and may report findings in a repository that passed under 6.0.0, the text report and two JSON fields changed, and Yggdrasil 6.1.0 pairs with Horde 6.1.0. The changelog lists each change that needs action under Before you upgrade and walks through them under Upgrading from 6.0.0; read both before upgrading.
The number stays 6.1.0 because the core of the family — Yggdrasil, Grain, Jarl and Horde — ships together under one number; Jarl, on 0.1.0 before, takes the family number from 6.1.0. A version then names one set of tools that were built and tested against each other, and a consumer that reads another tool's document can say which one it expects by naming that version. Numbering each tool by its own compatibility changes would split that number four ways within a release or two, and the documents in the register above already carry their own compatibility signal: their schema id, under the rule below. The version number names a set of tools tested together, and the changelog's Before you upgrade section says what an upgrade asks of you.
What that asks of you: pin the exact CLI version in CI (npx @chrisdudek/yg@6.1.0, or an exact version in package.json, never a caret range and never npx @chrisdudek/yg without a version), and raise the pin in a commit of its own after reading the changelog's Before you upgrade section. The same holds for the other family tools a pipeline runs.
What each producer fills
A document can declare more than its producers write. The register says who produces a document; this section says which of its declared kinds each producer actually fills, so a consumer does not have to rediscover the gap. grain-advice/1 declares four kinds:
relation— filled by Grain,grain advise.split— filled by Grain,grain advise.rule— filled by Grain,grain advise, since 6.1.0 (issue 446).port— no producer. Grain reads no aspects, so it has nothing to name a contract from.
Yggdrasil's yg advise import accepts all four kinds, because they are the graph's own vocabulary. A look-alike group (a set of similar files) does not travel in grain-advice/1 as a rule: an advice item names components in nodes, and a look-alike group can cut across components, so look-alike groups have their own document, one file per producer: .family-candidates.<producer>.json. That file name keeps the older word "family"; everywhere else on this page, "family" means the tool family.
The rule
Adding a field inside /N is free — a consumer that does not know the field ignores it, and every document here may grow that way without warning. Changing the shape of a field that already exists is /N+1, and a new family version with it. A consumer that is handed a document version it does not know refuses, and names the version to install rather than guessing: in Horde that refusal is failStaleCli in skills/horde/scripts/node.mjs, reached when ygJson sees a schema value that is not the one the call site asked for, and it prints the release to upgrade past. (The neighbouring failNoCli is a different refusal — no CLI at all — and says nothing about versions.)
A fourth sentence, because the alternative is a silent break: a change in what an existing field means counts as a shape change, even when the field's type is untouched. A list that used to be empty when a relation named no port, and is never empty now, tells a consumer something different under the same number.
Two corrections in 6.1.0 are recorded here so nobody reads them as silent meaning changes. yg-check/1 written by yg check --approve --dry-run --json said exit.code: 1 while the process exits 0; it now says what the process does (code: 0, the new exit.status value preview, the tree's own verdict kept in exit.reason), which is what the flag always promised. totals.draftSkipped always counted draft rules; its description said pairs, and now says rules. totals.verdicts.unverified counts only the pairs no verdict was ever recorded for; the judged-then-moved ones are totals.verdicts.stale, which the text report and issues also call unverified (cause stale), so a reader counting unverified pairs adds the two. That was always so and is now written down. coverage.nodeOwned, coverage.typeCovered and coverage.excluded are numbers in every document (typeCovered is 0 when type-level coverage is off); before 6.1.0 all three were null with the tier off. A reader that handled the null keeps working, and one that fell back to coverage.covered — which counts excluded files as covered — no longer has to (Horde's quality index reads the three terms; Grain reads no coverage field). The next object (its cost.reviewerCalls included), notes, requiresUser, remaining.needsUser / remaining.waitingOnReviewer, the keyed-by-earlier-release cause and the --compact form (marked compact: true) are additions.
In yg-check/1, the findings aspect-missing-rule-source, aspect-unexpected-rule-source and aspect-reference-broken now carry the rule they are about in aspect, as other rule findings do; before, the rule was named only inside what. An addition: nothing that was there changed.
In yg-check/1, each edge of a relation-undeclared-dependency finding now carries allowedRelations: the relation types the importing component may declare to that target to sanction the import (uses, calls, extends, implements — those the architecture allows between the two node types). An empty list means the architecture forbids every one of them, so no relation can be declared and the dependency has to go, or the architecture has to change with the user's approval. A consumer that told the two apart by the words of next ("no relation type is allowed") reads this field instead. An addition: nothing that was there changed, and the field is absent on every other finding's edges.
In yg-error/1, code is now always named by the command rather than inferred from the wording of what. The codes a consumer already reads keep their meaning — node-not-found, aspect-not-found and usage are given exactly where they were — with three corrections: a malformed --node value to yg log add|read|merge-resolve is node-path-invalid, a refusal of yg log merge-resolve names its reason (log-merge-not-in-progress, log-merge-conflict-markers, log-merge-out-of-order, …) and a refusal of yg log add --aspect --status (yg aspects log add --status before it was removed) names its own (aspect-status-not-standing, aspect-status-evidence-missing, …), each command-error before; yg aspect-test and yg drill over a check.mjs that exports check the wrong way are command-error, not usage; and yg impact over a lock that does not parse is lock-invalid. Every code is listed under Issue codes.
In yg-error/1 from yg pack add, update, remove, list, verify and new, a refusal over the record of installed packages, a package manifest or a marketplace manifest now carries the code its parser gives (packages-lock-invalid, packages-lock-package-invalid, package-requires-unsatisfied, marketplace-manifest-missing, package-name-mismatch, package-version-mismatch, …) instead of command-error; the same codes yg check and yg marketplace check already report for the same files. No consumer in the family runs a pack command (Horde and Grain were checked), and what, why and next are unchanged.
The load error for a rule with no rule source and no implies: is renamed from aspect-reviewer-missing to aspect-rule-source-missing: the rule is missing a content.md or check.mjs, not a reviewer: block. A finding in yg-check/1, and a yg-error/1 document, under a renamed code carries aliases, an additive list of the names it was reported under before (here ["aspect-reviewer-missing"]), the same device yg-advise/1 uses for its renamed items; a consumer matching the old name matches code or aliases. Grain's seam tests plant this load failure and name the old code; nothing Grain ships reads it.
That rule has one recorded exception, taken deliberately in 6.0.0. Normalising every relation onto a named port made the port lists in the component and blast-radius documents never empty, and the decision was to keep both documents on their existing numbers rather than bump them — the change is stated in the changelog and in each document's own reference instead. It is an exception on the record, not a precedent: the next silent meaning change takes a number.
6.1.0 broke that sentence twice, and both breaks are recorded here rather than left silent. Neither document took a new number, because no consumer in the family reads either field (Horde and Grain were checked for both), and a /2 would have made every consumer that reads the rest of the document refuse it:
suggestedNextinyg-check/1was the first finding'snexttext, verbatim and possibly several lines. It is now the report's one-linenext:step — the first step of the first block, annotated with what it costs (yg check --approve (unverified — 1 script pair · free + 1 reviewer pair · 1 call · paid)). A byte-compatible field was not possible either way: every finding'snexttext was reworded by the new output grammar. A consumer that wants the step as data reads the newnextobject (command,text,target,cost,requiresUser,remaining,then) and never parsessuggestedNext.- Two
yg-advise/1item ids changed with their class names:dead-attach:<rule>is nowaspect-effective-nowhere:<rule>(the check's own code for the same finding) anduncovered-hot-spot:<node>is nowunguarded-hot-spot:<node>. A consumer keying records by item id (Horde's audit ledger keysadvise:<id>) would see the renamed item as new. Each renamed item carriesaliases, an additive list of the ids it was known by, so a consumer can match an old record to it;yg advise dismissanddeferstill accept the old ids, and decisions stored under them keep applying. usage.impliedinyg-aspects/1counted every rule a component received through anything but its own list, its architecture type and a flow: another rule'simplies, an ancestor, and a consumed port. It now counts onlyimplies; the other two have their own additive fields,inheritedandport.
After 6.1.0, two more meanings of yg-check/1 fields changed without a new number, for the same reason: no consumer in the family reads either where it changed. Horde never reads next.target, and reads next.remaining only off a read-only report, never off an aborted fill; Grain reads neither (both were checked).
next.targetof a fill step (yg check --approve,yg check --approve --only-deterministic) named the first member of the block that leads the report, although the fill acts on every pending pair. It is now{}, as it already was foryg check --full.next.remainingin the document of a fill a gate aborted counted only the gating findings. It now counts every error of the tree, as it does in every other document, so it agrees withtotalsandissuesbeside it.
The published schemas
Every yg-…/1 document Yggdrasil writes or reads has a JSON Schema, published with these docs at /schemas/<id>.schema.json (yg-check/1 at /schemas/yg-check-1.schema.json, and so on). None is written by hand: a JSON document's schema is derived from the TypeScript type its emitter is compiled against, and the three package documents' from the file-format schema their parser enforces (yg schemas read package). A test regenerates all of them and fails when a published one differs, validates real documents emitted over a matrix of fixture repositories against them, and requires every field any of them declares to be named on this page or in the CLI Reference.
They describe exactly what this release writes: every mapping is closed (additionalProperties: false) and every enumeration lists the values this release uses. Use them to check a producer, or to learn a document's shape; do not use them to reject a document you read. Under the rule a later release may add a field, or a value, within /1, and a consumer that refused it would break on an upgrade that promised it would not.
The guard
Two halves, both deterministic, no network and no clock.
In Yggdrasil, source/cli/tests/unit/repo/family-contracts-invariant.test.ts reads every *_JSON_SCHEMA constant out of source/cli/src/formatters/ and source/cli/src/cli/ and requires each one on this page, and requires every yg-…/N id on this page to be either one of those constants or on a named whitelist of documents produced elsewhere. The assertion runs both ways on purpose: one direction alone lets a row for a document that no longer exists sit here forever.
Jarl's two contracts are described in Jarl's own manual (the record as a library, profiles) and guarded in Jarl itself, not against this page: record-contract.test.mjs fails when record.mjs's exports or their parameters change without a new RECORD_API, and the profile reader refuses a jarl-profile version it does not read. No check yet compares Jarl's rows with this page.
In Grain, the seam job — the only CI that has all three checkouts at once — scans Horde's scripts for the schema ids they read, collects the ids Grain writes, and requires every one of them on this page. Drift in any of the three repositories turns that job red.