---
title: Core Concepts
---

Yggdrasil stores verification rules in a graph under `.yggdrasil/`
in your repository. The graph has three main elements: nodes, aspects, and flows.

The agent creates and maintains these. You tell the agent what matters,
and it builds the graph structure.

---

## Directory structure

```text
.yggdrasil/
  yg-config.yaml          ← project config (reviewer, quality thresholds)
  yg-architecture.yaml    ← node type definitions
  model/                  ← nodes (your system's components)
    orders/
      order-service/
        yg-node.yaml
    payments/
      payment-service/
        yg-node.yaml
  aspects/                ← cross-cutting rules
    requires-audit/
      yg-aspect.yaml
      content.md
    rate-limiting/
      yg-aspect.yaml
      content.md
  flows/                  ← business processes
    checkout/
      yg-flow.yaml
  schemas/                ← YAML schemas (auto-generated by yg init)
```

---

## Nodes

A node represents a component in your system: a module, service, library,
or any cohesive piece of code. Each node is a directory under `.yggdrasil/model/`
with a `yg-node.yaml` file.

```yaml
# .yggdrasil/model/orders/order-service/yg-node.yaml
name: OrderService
type: service
description: "Manages order lifecycle: creation, validation, state transitions"

aspects:
  - requires-audit
  - rate-limiting

relations:
  - target: payments/payment-service
    type: calls

mapping:
  - src/orders/
  - src/orders.ts
```

**Key fields:**

- **type** — must match a type defined in `yg-architecture.yaml` (e.g. `module`,
  `service`, `library`)
- **aspects** — list of aspect IDs this node must satisfy
- **relations** — outgoing dependencies to other nodes (see [Relations](#relations)
  below)
- **mapping** — source files and directories this node owns (paths relative to
  repo root). Entries accept minimatch glob patterns: `*` matches within a single
  path segment, `**` matches across segments. For example,
  `src/db/*Repository.cs` maps only files matching that pattern in `src/db/`;
  `src/**/*.ts` maps all TypeScript files anywhere under `src/`. Plain entries
  (no glob metacharacters) behave as before: an exact file or a directory prefix
  that covers all files beneath it.

Nodes can be nested. Children inherit parent aspects automatically.

### Prompt size and the reviewer's context window

The LLM reviewer assembles each pair into one prompt (the rule, any reference
files, and the subject files). To keep that prompt inside a model's context
window, a tier in `yg-config.yaml` may set `max_prompt_chars`; `yg check`
measures the assembled prompt for each LLM pair and reports `prompt-too-large`
if it exceeds the tier's limit. The remedies, in safety order: narrow the
aspect's `scope.files` so non-target payload drops out, switch a file-local
rule to `per: file`, split the node, or raise the limit. Deterministic aspects
read files programmatically with no prompt, so they are never subject to this
gate. See [Configuration](/configuration#reviewer-tiers).

### Relations

Relations declare dependencies between nodes. They serve two purposes:
`yg impact` uses them to calculate blast radius, and `yg check` validates
that targets exist and port contracts are satisfied.

```yaml
relations:
  - target: payments/payment-service
    type: calls
  - target: shared/logger
    type: uses
```

Available types: `calls`, `uses`, `extends`, `implements`, `emits`, `listens`.
Relation types are constrained by the architecture — each node type declares
which target types it may reach per relation type, and `yg check` rejects
relations that violate those constraints. The event types `emits` and `listens`
must be paired.

### Ports

A node can declare **ports** — named entry points with required aspects.
When another node consumes a port, it must satisfy the port's aspects.

```yaml
# payments/payment-service/yg-node.yaml
ports:
  charge:
    description: "Charge a payment method"
    aspects: [correlation-tracking]
```

```yaml
# orders/order-service/yg-node.yaml
relations:
  - target: payments/payment-service
    type: calls
    consumes: [charge]
```

Now `orders/order-service` must satisfy the `correlation-tracking` aspect
because it consumes the `charge` port. This is the "port contracts" channel
in the aspect propagation table below.

### Minimal nodes (coverage without enforcement)

For parts of the codebase you don't need to enforce rules on, create a node
without aspects:

```yaml
name: LegacyAuth
type: module
description: "Legacy authentication system — do not modify"
mapping:
  - src/legacy/auth/
```

Nodes without aspects produce no pairs — there is nothing to verify and
nothing to record. They satisfy the coverage requirement without adding
enforcement overhead. When you're ready to enforce rules, add aspects to
the node.

---

## Aspects

An aspect is a cross-cutting architectural rule. It's a directory under
`.yggdrasil/aspects/` with a `yg-aspect.yaml` and one or more `.md` content files.

```yaml
# .yggdrasil/aspects/requires-audit/yg-aspect.yaml
name: Audit Logging
description: "Every mutation must emit an audit event"
```

```markdown
<!-- .yggdrasil/aspects/requires-audit/content.md -->
Every public mutation endpoint must emit an audit event before returning.

Use the shared `auditLog.emit()` utility. Do not build custom audit logic.
The event must include: user ID, action, timestamp, affected resource ID.
```

The content `.md` files are the actual rules. The reviewer reads them and checks
whether the source code satisfies them. Write them as clearly as you would write
a code review comment.

Aspects are verified by reviewers — Yggdrasil ships two reviewer types (LLM and deterministic).
See [Reviewers](/reviewers) for the decision matrix, authoring guides for `content.md`,
`check.mjs` (deterministic), suppression mechanics, and edge cases.

### The `implies` field

An aspect can declare that it implies other aspects. Implied aspects are included
recursively — if A implies B and B implies C, then a node with A automatically gets
B and C as well.

```yaml
# .yggdrasil/aspects/requires-audit/yg-aspect.yaml
name: Audit Logging
description: "Every mutation must emit an audit event"
implies:
  - requires-logging
```

Implied aspects must exist in the graph. Cycles are forbidden — `yg check` detects them.
Use implies when one rule logically requires another to be satisfied first. For example,
audit logging only makes sense if diagnostic logging is also active.

### Reference files

LLM aspects may declare supporting files via `references:` in
`yg-aspect.yaml`. The reviewer reads them as authoritative context;
the agent sees them under `read:` in `yg context`.

```yaml
references:
  - docs/error-codes.md
  - path: source/lib/codes.ts
    description: "Source of truth for error code constants."
```

Reference files are part of an LLM pair's input hash, so editing one
invalidates every pair of the aspect — re-verifying them all. Run
`yg impact --file <ref>` before editing a widely-referenced file.
References travel in every prompt, so under `per: file` scope they are
included once per file; the tier's `max_prompt_chars` bounds the result.

### Aspect scope and units

Every aspect with a rule source declares how broadly it verifies:

```yaml
# yg-aspect.yaml
scope:
  per: node | file          # default: node
  files:                     # optional — narrows which mapped files are reviewed
    all_of:
      - path: "src/**/*.ts"
      - not: { path: "**/*.test.ts" }
```

- **`per: node`** (the default) verifies the whole node in one pass — the LLM
  reviewer sees every subject file in one prompt; a deterministic check sees them
  all in one `check(ctx)` call.
- **`per: file`** verifies each subject file independently — one prompt (or one
  `check(ctx)` call) per file.

The thing one verification covers is a **unit**: the whole node under `per:
node`, a single file under `per: file`. An aspect paired with a unit is a
**pair** — the unit of work, cost, and caching. `scope.files` filters which
mapped files form the subject set; an aspect that excludes every file of a
node it lands on simply produces no pair there (a vacuous pass — `yg context
--node` shows the per-aspect subject count, including `0 files`).

Use `per: file` only for **file-local** rules ("every handler validates its
input"). Rules that need cross-file context ("exactly one file exports X",
"a correlation ID propagates across calls") must stay `per: node` — a per-file
reviewer cannot see the rest of the node.

### Aspect status

Each aspect carries a status — `draft`, `advisory`, or `enforced` — that
controls how its results render. Status never re-runs the reviewer; verdicts
survive every status flip.

| Status      | A refusal renders as | An unverified pair renders as | Blocks CI |
|-------------|----------------------|-------------------------------|-----------|
| `draft`     | pair not expected    | pair not expected             | no        |
| `advisory`  | warning              | warning                       | no        |
| `enforced`  | error                | error                         | yes       |

Status defaults to `enforced`. Set `status: advisory` on a new rule to
gather signal across the repo without blocking CI; promote to `enforced`
once you trust it. `draft` removes the aspect's pairs entirely. Status can be
set on the aspect itself or overridden on any attach site (node, type, flow,
port). The effective status on a node is the strictest declared across all
channels — bumping up is fine, silent downgrades are rejected.

See [Aspect Status](/aspect-status) for the full lifecycle, `status_inherit`
on implies edges, severity-by-status, and verdict reuse across flips.

### How aspects reach nodes

Aspects propagate through seven channels:

| Channel       | How it works                                                |
|---------------|-------------------------------------------------------------|
| Own           | Listed in `aspects:` field of `yg-node.yaml`                |
| Ancestor      | Parent node aspects inherited by all children               |
| Own type      | Default aspects defined per node type in architecture file  |
| Ancestor type | Architecture defaults from parent node types                |
| Flows         | All flow participants inherit flow-level aspects            |
| Ports         | Consumer must satisfy port-required aspects                 |
| Implied       | Aspects included via `implies` chains (recursive)           |

One aspect can reach dozens of nodes through these channels. When you change
an aspect's content, every pair it produces is flagged for re-verification.

---

## Flows

A flow describes a business process that spans multiple nodes. It's a directory
under `.yggdrasil/flows/` with a `yg-flow.yaml`.

```yaml
# .yggdrasil/flows/checkout/yg-flow.yaml
name: Checkout
description: "Customer places order, payment is processed, inventory reserved"
nodes:
  - orders/order-service
  - payments/payment-service
  - inventory/inventory-service
aspects:
  - correlation-tracking
```

Flow-level aspects (like `correlation-tracking` above) propagate to all
participants. This is useful for cross-cutting requirements that apply
to an entire business process.

---

## The lock and the enforcement loop

Every verdict — LLM and deterministic alike — is stored as a content-addressed
entry in one committed file, `.yggdrasil/yg-lock.json`. Each entry maps an
`(aspect, unit)` pair to a verdict plus the hash of the inputs that produced it
(the rule text, the subject files, and for LLM pairs the tier and references).
A verdict is valid exactly while those inputs still hash to the stored value;
any change to them makes the pair **unverified**.

1. Agent reads context before writing code (`yg context --file <path>`)
   and sees which aspects apply
2. Agent writes code
3. Agent runs `yg check --approve` — it fills every unverified pair.
   Deterministic pairs run first, locally, for free; LLM pairs then go to the
   reviewer with `content.md` and the subject files
4. Reviewer says pass → an `approved` verdict is recorded in the lock
5. Reviewer says fail → a `refused` verdict is recorded and rendered; the agent
   fixes the code and re-runs `yg check --approve`

`yg check` is the CI gate. It recomputes each pair's input hash and compares it
against the lock — no LLM calls, no provider keys, runs instantly. If a pair's
inputs changed since its verdict was recorded, the hash no longer matches and
check reports it as unverified. The agent does the verification locally; CI just
confirms the recorded verdicts still hold.

Refusals are cached like approvals: re-running `yg check --approve` over a
refused pair with unchanged inputs does not re-run the reviewer. The three ways
out of a refusal are fix the code, sharpen the rule (which re-verifies every
pair of the aspect — check `yg impact --aspect` first), or, with your sign-off,
add a `yg-suppress` marker.

---

## Architecture file

`yg init` creates `.yggdrasil/yg-architecture.yaml` with an empty
`node_types: {}` and commented examples — types are defined per project.
This file controls what types of nodes are allowed and can set default
aspects per type. You define the types your project needs; the agent knows
the schema and can modify this file on your behalf.

```yaml
node_types:
  module:
    description: "Business logic unit with clear domain responsibility"
  service:
    description: "Component providing functionality to other nodes"
    aspects: [requires-audit]
    log_required: true        # opt in — service changes carry business intent
    enforce: strict
    parents: [module]
    relations:
      calls: [service, library]
      uses: [library]
    when:
      path: "src/**/*.service.ts"
  library:
    description: "Shared utility code with no domain knowledge"
    when:
      path: "src/shared/**"
```

**Fields per type:**

- **`description`** — Human-readable label shown in `yg tree` and `yg type-suggest` output.

- **`aspects`** — Default aspects automatically applied to every node of this type.
  This is one of the seven aspect distribution channels ("by node type"). Use it for
  cross-cutting rules that all nodes of a type must satisfy — e.g. every `service` must
  have audit logging. Run `yg impact --type <id>` before adding an aspect here to see
  how many nodes will be affected.

- **`log_required`** — Whether `yg check --approve` requires a fresh log entry
  before it records a verdict for a source change on a node of this type. Defaults
  to `false` (opt-in). Enable it on types whose changes carry business intent worth
  capturing — domain logic, command handlers — so the *why* behind each change is
  recorded before it is verified.

- **`enforce: strict`** — When set to the string literal `strict`, every source file
  matched by the type's `when` predicate must belong to exactly one node of this type.
  Files matched by `when` but not in any node's `mapping:` produce a `type-strict-orphan`
  error in `yg check`; files mapped to a node of the wrong type produce a
  `type-strict-misplaced` error. Use this to prevent new files from being added to the
  codebase without graph coverage.

- **`parents`** — List of type IDs that nodes of this type may nest under. If specified,
  `yg check` rejects nodes placed under an unlisted parent type. Omit to allow any parent.

- **`relations`** — Map of relation type → allowed target types. Restricts which node
  types a node of this type may declare relations to. `yg check` rejects relations
  that violate these constraints.

- **`when`** — Predicate that classifies files into this type. Can match on `path`
  (glob pattern) and/or `content` (text patterns). Used by `yg type-suggest` and by
  `enforce: strict` backward coverage. See [Conditional Aspects](/conditional-aspects)
  for the full predicate grammar — the same grammar applies here. A type with no `when`
  is organizational: it can act as a hierarchy parent but its nodes cannot have a
  non-empty `mapping:`.

---

## Next

- [Reviewers](/reviewers) — LLM and deterministic reviewer types, authoring guides, suppression, edge cases
- [Conditional Aspects](/conditional-aspects) — `when` predicates for selective aspect application
- [CLI Reference](/cli-reference) — full command surface
