Skip to content

Aspect Status

Yggdrasil aspects ship with three enforcement levels: draft, advisory, enforced. Status is purely how results render. It never decides whether a verdict is computed or re-used — only whether a pair is expected at all (draft removes it) and whether a problem blocks CI. It is the dial you turn as a rule matures: start silent, gather signal, then enforce.

Three levels

StatusA refusal renders asAn unverified pair renders asBlocks yg check?
draftpair not expectedpair not expectedno
advisorywarningwarningno
enforcederrorerroryes

draft aspects produce no pairs at all — there is nothing to verify and nothing in the lock. advisory and enforced pairs are both verified and cached the same way; the level only changes severity. Severity follows status uniformly: advisory never blocks (whether a pair is refused or merely unverified), and enforced always blocks.

When to use each

  • draft — content.md / check.mjs is still being authored, or the rule is unclear. Zero cost, zero expected pairs. Use this while iterating on the rule text before any node has a real verdict. draft is also the only way to park an aspect without a provider key — it removes the pairs rather than leaving them red. For LLM aspects with a companion.mjs hook: when the aspect is draft, the hook never runs.
  • advisory — rule is complete; gather signal across the repo without blocking CI. Pairs are verified and cached normally; refusals and unverified pairs surface as warnings. Use this to measure how often a rule fires on real code before promoting it.
  • enforced — rule is vetted; violations should block CI. Refusals and unverified pairs are errors that block yg check.

A typical lifecycle: draft while authoring → advisory for one or two sprints to observe behavior → enforced once you have confidence the rule fires only on real violations.

Declaring status

status: can appear on the aspect itself (aspect-level default) and on every object-form attach site. The aspect-level default applies wherever the aspect attaches unless an attach site declares its own.

Aspect-level default

yaml
# .yggdrasil/aspects/audit-logging/yg-aspect.yaml
name: Audit Logging
description: "Every mutation emits an audit event"
status: advisory               # default for every attach site
reviewer:
  type: llm

If status: is omitted, the aspect defaults to enforced.

Per-node attach site

yaml
# .yggdrasil/model/orders/yg-node.yaml
aspects:
  - id: audit-logging
    status: enforced           # promote on this node only

Bare-string entries (- audit-logging) inherit the aspect-level default. Object form ({ id, status }) is the only way to override per site.

Architecture node-type default

yaml
# .yggdrasil/yg-architecture.yaml
node_types:
  command:
    aspects:
      - id: cli-contract
        status: enforced

Flow-level

yaml
# .yggdrasil/flows/checkout/yg-flow.yaml
aspects:
  - id: correlation-tracking
    status: advisory

Port-level

yaml
# .yggdrasil/model/payments/yg-node.yaml
ports:
  charge:
    aspects:
      - id: idempotency
        status: enforced

How effective status is computed

For each (node, aspect) pair, the resolver collects every channel that attaches the aspect (after when filtering), then takes the maximum:

effective_status = max(declared in each channel)

where draft < advisory < enforced. The strictest level wins.

If an attach site explicitly declares a status lower than what the cascade would yield from other channels (plus the aspect-level default), the validator emits aspect-status-downgrade as an error. The rule is bump up OK, downgrade is an error: a node can promote an aspect to enforced, but it cannot quietly weaken what the architecture or a flow demands.

To resolve a downgrade error: remove the explicit lower status from the attach site, or raise the lower-ranked channel to match.

This max() computation and the downgrade check apply to the cascading attach channels 1–6 (own, ancestor, own type, ancestor type, flows, ports). Channel 7 (implies) does not declare a status: — it carries status_inherit: instead, described in the next section.

Implies propagation

Terminology note: In the implies: mechanism, aspects implied by another aspect are sometimes informally called "companions" in prose (as in "an implies bundle promotes its implied companions"). This is a different concept from companion files — the optional companion.mjs hook on an LLM aspect that resolves per-unit files for the reviewer. This page uses "implied sibling" or "implied aspect" to avoid ambiguity.

For aspect A that implies aspect B, propagation to a node depends on A's effective status on that node and the status_inherit: modifier on the implies edge.

Draft is dormant

If A's effective status on node N is draft, then B is not propagated via implies. Draft aspects have zero side effects. B may still arrive on N through any other channel.

Active implier — status_inherit modes

yaml
# .yggdrasil/aspects/audit-logging/yg-aspect.yaml
implies:
  - id: diagnostic-logging
    status_inherit: strictest        # default

Two modes:

  • strictest (default) — B contributes max(A_effective, B_default) to the cascade. An implies bundle promotes its implied siblings. If A runs as enforced and B defaults to advisory, B becomes enforced on that node.
  • own-default — B contributes B_default regardless of A. Use this to decouple propagation when an implied aspect should retain its own status even when reached through a stricter implier.
yaml
implies:
  - id: diagnostic-logging
    status_inherit: own-default      # keep B at its own default

Bare-string implies entries (implies: [diagnostic-logging]) inherit the strictest default.

Status and verdicts

Status is deliberately not part of a pair's input hash. Verdicts survive every status flip — flipping advisory ↔ enforced (or a full draft round-trip and back) never re-runs the reviewer; the recorded verdict carries forward as long as the pair's real inputs are unchanged.

What each transition does:

  • draft → advisory/enforced — the aspect's pairs become expected. If they were never verified, they appear as unverified until yg check --approve fills them. If they already hold a valid verdict (a draft round-trip), they are instantly re-used — no reviewer call.
  • advisory → enforced — re-uses the existing verdict; nothing re-runs. But if that verdict is a refusal, the pair now renders as an error instead of a warning, so a passing check can turn red. Promote intentionally and expect any recorded refusal to surface.
  • enforced/advisory → draft — the aspect's pairs leave the expected set. Their lock entries are kept (verdicts survive), so returning to enforced re-uses them; garbage collection only prunes a pair that is genuinely gone (aspect detached, file deleted, when now false), never one parked at draft.

when is not status

The when predicate decides whether an aspect applies to a node. Status decides what happens when it applies. They are orthogonal:

  • when=false → aspect is invisible on the node. No pair, no verdict, no display in yg context. Costs nothing.
  • status=draft → aspect applies but produces no expected pair. Appears in yg context, no reviewer call, no recorded verdict.

Use when to model applicability (this rule only applies to nodes that call an external service). Use status to model rule maturity (this rule is still being authored / observed / enforced).

See Conditional Aspects for the when predicate grammar.

See also