Appearance
Family contracts
Yggdrasil, Grain and Jarl 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 or Grain; 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 | no external consumer | before 6.0.0 | CLI Reference |
Context package (yg context --json) | yg-context/1 | Yggdrasil | no external consumer | 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 | no external consumer | before 6.0.0 | CLI Reference |
Component (yg node --json) | yg-node/1 | Yggdrasil | no external consumer | before 6.0.0 | CLI Reference |
Rule list (yg aspects --json) | yg-aspects/1 | Yggdrasil | no external consumer | before 6.0.0 | CLI Reference |
Attention feed (yg advise --json) | yg-advise/1 | Yggdrasil | no external consumer | 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 | no external consumer | 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 | no external consumer | 6.0.0 | CLI Reference |
Drill run (yg drill --json) | yg-drill/1 | Yggdrasil | 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 | no external consumer | 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 | no external consumer | 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 | 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 | 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 | 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 | Yggdrasil — yg advise import | before 6.0.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 | no external consumer | before 6.0.0 | this page |
Co-change partners (grain completeness --json) | grain-completeness/1 | Grain | no external consumer | 6.1.0 | this page |
Co-change and cut score (grain cochange --json) | grain-cochange/1 | Grain | no external consumer | 6.1.0 | this page |
Territory measurement (grain measure --json) | grain-measure/1 | Grain | no external consumer | 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 | no external consumer | 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 | 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 | 6.1.0; report --json carries metrics, with check: whether the loop lands on a declared check or on testimony | 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, and the text report and two JSON fields changed. 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 and Jarl, 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 three 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.
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 (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 (Grain was 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 (Grain was 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 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. Grain reads neither (it was 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 both checkouts at once, collects the schema ids Grain writes and requires every one of them on this page. Drift in either repository turns that job red.