Skip to content

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 ​

DocumentSchema idProducerConsumersSinceDescribed where
Graph files (.yggdrasil/model/**/yg-node.yaml, yg-architecture.yaml)no schema id — the graph's own formatYggdrasil, and whoever edits the graphGrain — grain advise and grain propose over a graph that already exists read them directlybefore 6.0.0Configuration
Run report (yg check --json)yg-check/1YggdrasilHorde — the rule ladder (node.mjs promote, demote, ladder) and the wave close's quality indexbefore 6.0.0CLI Reference
Context package (yg context --json)yg-context/1YggdrasilHorde — node resolution, land; neither reads the log fieldsbefore 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.0CLI Reference
Blast radius (yg impact --json)yg-impact/1YggdrasilHorde — port consumersbefore 6.0.0CLI Reference
Component (yg node --json)yg-node/1YggdrasilHorde — node existence and structurebefore 6.0.0CLI Reference
Rule list (yg aspects --json)yg-aspects/1YggdrasilHorde — init, the rule ladder and the quality index, the legislator's brief, law, landbefore 6.0.0CLI Reference
Attention feed (yg advise --json)yg-advise/1YggdrasilHorde — auditbefore 6.0.0CLI Reference
Rule history (yg log read --aspect --json; yg aspects log read --json before 6.1.0)yg-aspect-log/1YggdrasilHorde — lawbefore 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/1YggdrasilHorde — land6.0.0CLI Reference
Drill run (yg drill --json)yg-drill/1YggdrasilHorde — 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.0CLI Reference
Rule health (yg aspects --health --json) — its own document, not the rule listyg-aspects-health/1YggdrasilHorde — the wave close's audit (quiet rules)6.1.0CLI 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 takesyg-error/1YggdrasilHorde — every document read (node.mjs ygJson; it reads code, what, why and next.text, never next.command)6.1.0CLI Reference
Node list (yg tree --json)yg-tree/1Yggdrasilno external consumer6.1.0CLI Reference
File owner (yg owner --json)yg-owner/1Yggdrasilno external consumer6.1.0CLI Reference
File owner batch (yg owner --files --json)yg-owner-batch/1Yggdrasilno external consumer today — built for Horde's territory resolver, one node or type:<id>@<top-level dir> unit per file (447)6.1.0CLI Reference
Search results (yg find --json)yg-find/1Yggdrasilno external consumer6.1.0CLI Reference
Component log (yg log read --node --json)yg-log/1Yggdrasilno external consumer6.1.0CLI Reference
Type decisions (yg log read --type --json) — a node type's decision logyg-type-log/1Yggdrasilno external consumer (Horde writes decisions with yg log add --type; it reads none back)6.1.0CLI Reference
Marketplace manifest (yg-marketplace.yaml)yg-marketplace/1the marketplace author; Yggdrasil's yg marketplace init writes it and yg pack new adds an entryYggdrasil — yg pack add / update / verify / new, yg marketplace check (yg pack list asks the source for tags and never reads it)6.0.0Packages
Package manifest (yg-package.yaml)yg-package/1the package authorYggdrasil — yg pack, yg marketplace check6.0.0Packages
Package record (.yggdrasil/yg-packages.yaml) — what is installed, not a verdict lockyg-packages/1Yggdrasil — yg packYggdrasil — the package-file-modified rail and yg advise; no external consumer6.0.0; the optional requested, tag, commit and identity fields arrived in 6.1.0, and a record without them is still readPackages
Refused-content record (.yggdrasil/.refused/<hash>.json, local and gitignored)versioned by v: 1; joined to the event line and the lock entry by hashYggdrasil — yg check --approve, one file per refusal it recordsGrain — the refusal→fix pairs a rule's own verdicts give (a reader, not yet shipped)6.1.0Reviewers
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: 1Yggdrasil — yg check --approveGrain — 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 tooConfiguration
Advice (grain advise --json)grain-advice/1GrainHorde — 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 importbefore 6.0.0; the legislator's brief reads it since 6.1.0this page
Proposed graph (proposal.json)grain-proposal/1Grain — proposeYggdrasil — yg adoptbefore 6.0.0this page
Measured breakage per proposed rule (aspects/<id>/provenance.json in a proposal)no schema id — one file per ruleGrain — proposeYggdrasil — yg adopt, which reads existingViolations for its "Already broken" countbefore 6.0.0this page
Package versions cache (.yggdrasil/.yg-packages-versions.json)yg-package-versions/1Yggdrasil — the package commandsYggdrasil — yg advise; no external consumer6.0.0CLI Reference
Repository exportgrain-export/1Grain — grain exportadopter pipelines and audits; no external consumer inside the familybefore 6.0.0this page
Convention check (grain check --json, grain review --json)grain-check/1Grainno external consumerbefore 6.0.0this page
Obligation (grain obligation --json)grain-obligation/1GrainHorde — tk newbefore 6.0.0this page
Co-change partners (grain completeness --json)grain-completeness/1GrainHorde — the architect, planned (it does not read the document yet)6.1.0this page
Co-change and cut score (grain cochange --json)grain-cochange/1GrainHorde — 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 cut6.1.0this page
Territory measurement (grain measure --json)grain-measure/1GrainHorde — 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/16.1.0this page
Proposal report (grain propose --json)grain-propose/1Grainno external consumerbefore 6.0.0this page
Oracle record (oracle.json)grain-oracle/1Grain — grain oracle recordGrain's own scoring; no external consumerbefore 6.0.0this page
Adopter correction inside an oracle recordgrain-correction/1Grain — grain oracle recordGrain's own scoring; no external consumerbefore 6.0.0this page
Oracle file list (files.json)grain-oracle-files/1Grain — grain oracle recordGrain's own scoring; no external consumerbefore 6.0.0this page
Oracle scorecard (grain oracle score --json)grain-oracle-score/1Grainno external consumerbefore 6.0.0this page
Look-alike groups (.family-candidates.<producer>.json; the file name keeps the older word "family")no schema id — versioned by v: 1two, 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 readYggdrasil — 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 sectionbefore 6.0.0this 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 constantjarl-record/1JarlHorde — 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 differs6.1.0this 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 gatejarl-profile/1 — no schema field; versioned by "jarl-profile": 1whoever 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 read6.1.0this page
Loop views (status, resume, report, queue, tips, list and show, each with --json) — one JSON block each, fields listed in Jarl's manualno schema id — described field by field in Jarl's manualJarldashboards and scripts outside the family; Horde reads the same data through jarl-record/1 (statusData, resumeData), never the command output6.1.0; report --json carries metrics, with check: whether the loop lands on a declared check or on testimonythis page
Law diffhorde-law/1Horde — lawthe session6.0.0this page
Mission retrospectivehorde-retro/1Horde — retrothe session; no external consumer6.0.0this page
Mission planhorde-plan/1Horde — queue planHorde itself; no external consumerbefore 6.0.0this page
Drill casehorde-drill-case/1Horde — drillHorde itself; no external consumerbefore 6.0.0this page
Mission report (report.mjs --json; rendered as hordes/<h>/report.md, rewritten on tick, wave close and ask)horde-report/1Horde — reportthe client, through the rendered report.md; no machine consumer in the family6.1.0; the measure field (the grain-measure/1 reading: before, after, range) arrived with itthis 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:

  • suggestedNext in yg-check/1 was the first finding's next text, verbatim and possibly several lines. It is now the report's one-line next: 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's next text was reworded by the new output grammar. A consumer that wants the step as data reads the new next object (command, text, target, cost, requiresUser, remaining, then) and never parses suggestedNext.
  • Two yg-advise/1 item ids changed with their class names: dead-attach:<rule> is now aspect-effective-nowhere:<rule> (the check's own code for the same finding) and uncovered-hot-spot:<node> is now unguarded-hot-spot:<node>. A consumer keying records by item id (Horde's audit ledger keys advise:<id>) would see the renamed item as new. Each renamed item carries aliases, an additive list of the ids it was known by, so a consumer can match an old record to it; yg advise dismiss and defer still accept the old ids, and decisions stored under them keep applying.
  • usage.implied in yg-aspects/1 counted every rule a component received through anything but its own list, its architecture type and a flow: another rule's implies, an ancestor, and a consumed port. It now counts only implies; the other two have their own additive fields, inherited and port.

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.target of 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 for yg check --full.
  • next.remaining in 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 with totals and issues beside 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.