Skip to main content

Advanced

6 min read

Definition Meta

Add optional metadata to any definition for better debugging, richer devtools, and self-documenting systems.

Meta is supported on all 7 definition types: modules, facts, events, constraints, resolvers, effects, and derivations.


Why Meta?

Directive's inspect(), explain(), and devtools show definition IDs like auth::needsLogin or fetchUser. Meta lets you attach human-readable context – labels, descriptions, categories, and colors – so these surfaces are immediately useful without checking source code.

Meta is purely informational. It is never read during reconciliation – zero runtime overhead.


Adding Meta

Every definition type (constraints, resolvers, effects, derivations) accepts an optional meta field.

Modules

const authModule = createModule("auth", {
  schema: { /* ... */ },
  meta: {
    label: "Authentication",
    description: "Handles login, tokens, and session management",
    category: "auth",
  },
  // ... constraints, resolvers, etc.
});

Module meta surfaces in inspect().modules and system.meta.module(id). Useful for architecture diagrams, module health dashboards, and onboarding documentation.

Facts (Schema Fields)

schema: {
  facts: {
    score: t.number().meta({ label: "Player Score", category: "data" }),
    email: t.string().meta({ label: "Email", tags: ["pii"] }).nullable(),
    plain: t.number(),  // no meta — works as before
  },
}

The .meta() method chains with all schema builder methods (.nullable(), .min(), .maxLength(), etc.). Surfaces in inspect().facts and system.meta.fact(key). Use tags: ["pii"] to mark fields for compliance scanning.

Events

events: {
  // Function form (no meta) — unchanged
  increment: (facts) => { facts.count += 1; },

  // Object form with meta
  reset: {
    handler: (facts) => { facts.count = 0; },
    meta: { label: "Reset Counter", category: "ui" },
  },
},

Event meta surfaces in inspect().events and system.meta.event(name).

Constraints

constraints: {
  needsLogin: {
    when: (facts) => !facts.user,
    require: { type: "LOGIN" },
    meta: {
      label: "Requires Authentication",
      description: "User is not logged in",
      category: "auth",
      color: "#f59e0b",
    },
  },
},

Resolvers

resolvers: {
  login: {
    requirement: "LOGIN",
    resolve: async (req, context) => { /* ... */ },
    meta: {
      label: "OAuth Login Flow",
      description: "Exchanges authorization code for access token",
    },
  },
},

Effects

effects: {
  logPhase: {
    run: (facts) => console.log(facts.phase),
    meta: { label: "Phase Logger", category: "logging" },
  },
},

Derivations

Derivations support two forms. The existing function form works exactly as before. The new object form uses compute instead of a bare function:

derive: {
  // Function form (no meta) – unchanged
  isReady: (facts) => facts.status === "ready",

  // Object form with meta
  displayName: {
    compute: (facts) => `${facts.firstName} ${facts.lastName}`,
    meta: { label: "Display Name", description: "Full name for UI" },
  },
},

The DefinitionMeta Type

import type { DefinitionMeta } from "@directive-run/core";

interface DefinitionMeta {
  label?: string;       // Human-readable name
  description?: string; // Longer explanation
  category?: string;    // Grouping key (e.g., "auth", "data", "ui")
  color?: string;       // CSS hex color (e.g., "#f59e0b")
  tags?: string[];      // Multi-dimensional labels for filtering
  [key: string]: unknown; // Extensible for plugins
}

You don't need to import DefinitionMeta to use it – inline objects are inferred correctly. The import is useful when building reusable helpers or plugins.

Suggested Categories

CategoryUse for
authAuthentication and authorization
dataData fetching and persistence
uiUI state and display logic
loggingLogging and analytics
lifecycleSystem lifecycle management

Categories are freeform strings – custom categories work, these are conventions for consistency.

Tags

Use tags for multi-dimensional filtering. A constraint can be both "auth" and "critical":

constraints: {
  needsLogin: {
    when: (facts) => !facts.user,
    require: { type: "LOGIN" },
    meta: {
      label: "Requires Auth",
      category: "auth",
      tags: ["critical", "security"],
    },
  },
},

Tags are separate from category – use category for primary grouping, tags for cross-cutting concerns.


Where Meta Surfaces

system.meta (O(1) queries)

Query meta directly by definition ID without building a full inspection snapshot:

// O(1) lookups
system.meta.module("auth")?.label;             // "Authentication"
system.meta.constraint("needsLogin")?.label;   // "Requires Auth"
system.meta.resolver("login")?.description;    // "Exchanges code for token"
system.meta.effect("log")?.category;           // "logging"
system.meta.derivation("displayName")?.tags;   // ["ui", "critical"]

// Returns undefined for unknown IDs
system.meta.constraint("nonexistent");         // undefined

// Bulk queries across all definition types
system.meta.byCategory("auth");   // MetaMatch[] — all auth definitions
system.meta.byTag("pii");         // MetaMatch[] — all PII-tagged fields
system.meta.byTag("critical");    // MetaMatch[] — all critical definitions

Each MetaMatch contains { type, id, meta } where type is "module" | "fact" | "event" | "constraint" | "resolver" | "effect" | "derivation".

Use byTag("pii") for compliance scanning, byCategory("auth") for architecture dashboards.

Use system.meta in plugins, custom devtools, and AI tooling for efficient meta access.

inspect()

system.inspect() includes meta on all seven definition types:

const inspection = system.inspect();

for (const c of inspection.constraints) {
  console.log(c.id, c.meta?.label ?? c.id);
}

for (const r of inspection.resolverDefs) {
  console.log(r.id, r.meta?.label ?? r.id);
}

for (const e of inspection.effects) {
  console.log(e.id, e.meta?.label ?? e.id);
}

for (const d of inspection.derivations) {
  console.log(d.id, d.meta?.label ?? d.id);
}

// Modules too
for (const m of inspection.modules) {
  console.log(m.id, m.meta?.label ?? m.id);
}

explain()

system.explain() uses meta.label instead of the raw constraint ID and includes meta.description when present:

Without meta:

Requirement "LOGIN" (id: req-1)
├─ Produced by constraint: needsLogin
├─ Constraint priority: 0

With meta:

Requirement "LOGIN" (id: req-1)
├─ Produced by constraint: Requires Authentication
├─ Constraint priority: 0
├─ Description: User is not logged in

Devtools

The devtools plugin reads meta from inspect() and uses it for:

  • Node labels in the constraint/resolver graph
  • Color coding by category
  • Tooltips with descriptions

Trace Enrichment

When trace: true is enabled, every trace entry automatically carries meta inline on its sub-arrays:

const sys = createSystem({ module: mod, trace: true });
sys.start();

// After reconciliation...
const entry = sys.trace![0];
entry.constraintsHit[0].meta?.label;     // "Requires Auth"
entry.resolversStarted[0].meta?.label;   // "OAuth Login Flow"
entry.factChanges[0].meta?.label;        // "Player Score"
entry.effectsRun[0].meta?.label;         // "Phase Logger"
entry.derivationsRecomputed[0].meta?.label; // "Display Name"

Meta flows with the causal event – trace viewers show human-readable labels without secondary lookups.


AI Agent Context

The @directive-run/ai package can inject system meta into LLM prompts automatically:

import { createAgentOrchestrator } from "@directive-run/ai";

const orchestrator = createAgentOrchestrator({
  runner,
  metaContext: true, // Inject system meta into agent instructions
});

Or build context manually with toAIContext() and formatSystemMeta():

import { toAIContext } from "@directive-run/ai";

const context = toAIContext(system);
// Returns formatted markdown with modules, constraints, resolvers,
// facts, events, effects, derivations — only annotated definitions

The output is token-efficient – sections with no annotated definitions are omitted entirely.


Dynamic Definitions

Meta works with dynamically registered definitions:

system.constraints.register("dynamicCheck", {
  when: (facts) => facts.score < 0,
  require: { type: "FIX_SCORE" },
  meta: { label: "Score Validator", category: "data" },
});

Security

Meta objects are frozen at registration time using Object.create(null) + Object.freeze. This prevents:

  • Prototype pollution (no __proto__ chain)
  • Mutation after registration
  • Accidental shared state between definitions

Production Bundles

Meta values are string literals that survive minification and ship in production bundles. This is by design – meta powers production features like AI agent reasoning, compliance scanning (tags: ["pii"]), and observability dashboards.

Avoid putting sensitive information in meta fields:

  • Internal API paths (e.g., /api/internal/auth/refresh)
  • Employee names or personal information
  • Sensitive business rules you don't want visible in client bundles

Use generic labels instead: "Auth Check" rather than "Refreshes JWT via /api/internal/auth/refresh".


Next Steps

Previous
Error Boundaries

Stay in the loop. Sign up for our newsletter.

We care about your data. We'll never share your email.

Powered by Directive. This signup uses a Directive module with facts, derivations, constraints, and resolvers – zero useState, zero useEffect. Read how it works

Directive - Constraint-Driven Runtime for TypeScript | AI Guardrails & State Management