Skip to content

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 ​

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/1Yggdrasilno external consumerbefore 6.0.0CLI Reference
Context package (yg context --json)yg-context/1Yggdrasilno external consumerbefore 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/1Yggdrasilno external consumerbefore 6.0.0CLI Reference
Component (yg node --json)yg-node/1Yggdrasilno external consumerbefore 6.0.0CLI Reference
Rule list (yg aspects --json)yg-aspects/1Yggdrasilno external consumerbefore 6.0.0CLI Reference
Attention feed (yg advise --json)yg-advise/1Yggdrasilno external consumerbefore 6.0.0CLI Reference
Rule history (yg log read --aspect --json; yg aspects log read --json before 6.1.0)yg-aspect-log/1Yggdrasilno external consumerbefore 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/1Yggdrasilno external consumer6.0.0CLI Reference
Drill run (yg drill --json)yg-drill/1YggdrasilGrain — 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/1Yggdrasilno external consumer6.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/1Yggdrasilno external consumer6.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 consumer6.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 consumer6.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 commandbefore 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/1GrainYggdrasil — yg advise importbefore 6.0.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/1Grainno external consumerbefore 6.0.0this page
Co-change partners (grain completeness --json)grain-completeness/1Grainno external consumer6.1.0this page
Co-change and cut score (grain cochange --json)grain-cochange/1Grainno external consumer6.1.0this page
Territory measurement (grain measure --json)grain-measure/1Grainno external consumer6.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/1Jarlno external consumer6.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 loopJarl — 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 family6.1.0; report --json carries metrics, with check: whether the loop lands on a declared check or on testimonythis 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:

  • 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 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. Grain reads neither (it was 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 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.