Morphic Agent Framework

domain

A logical area of the problem, and an explicit statement of what belongs to it.

export domain Rendering:
    description: Turning a document into pixels.

    covers:
        - The scene graph and its traversal
        - Rasterisation and caching
        - Colour management

A domain is a region of the problem. Rendering. Billing. Export. Identity.

It is not a folder, though it often has one. It is not a module, though it usually contains several. It is the answer to "which part of this system is that?" — and the reason it exists as a definition is that the answer is frequently non-obvious, and getting it wrong puts code in the wrong place forever.

coversstring[], required

The boundary, written as a list of what is inside it.

The purpose of covers is decidability. Someone — a person, an agent — has a new piece of work and needs to know whether it belongs here. covers should let them answer that without asking you.

Which means writing it at the level of concerns, not files:

# No. This is a directory listing with extra steps.
covers:
    - src/render/scene.ts
    - src/render/raster.ts
    - src/render/color.ts

# Yes. This tells you where a new thing goes.
covers:
    - The scene graph and its traversal
    - Rasterisation and caching
    - Colour management

The first version is wrong the moment somebody adds a file. The second one still decides correctly for code that does not exist yet.

How many

Three to seven, for most systems.

The test is: would you assign work to it? "Can you take a look at rendering this sprint" is a sentence people say. "Can you take a look at the colour management utility module" is not — that is a component.

Fewer than three and you have not partitioned anything. More than about eight and your domains have become components, or your concept is hiding two products.

Domains do not nest

There is no sub-domain. If you find yourself wanting one, you want either

  • a broader covers list on the parent domain, or
  • a component inside it.

Flat is deliberate. A nested taxonomy is a thing people spend a week arguing about and then never consult, and an agent has no way to know which level is authoritative.

Examples

A desktop application

use "@psy/morphic"

export domain Rendering:
    description: Turning a document into pixels.

    covers:
        - The scene graph and its traversal
        - Rasterisation and caching
        - Colour management and profile conversion

    excludes:
        - Anything that mutates the document. Rendering is read-only.

    invariants:
        - The renderer is pure: same document, same pixels
        - No rendering code imports from Tools

export domain Tools:
    description: Everything the user draws and edits with.

    covers:
        - Tool activation and modal state
        - Hit testing and selection
        - The command stack and undo

    excludes:
        - Drawing to the screen. Tools mutate the document; the renderer draws.

export domain Files:
    description: Reading and writing documents.

    covers:
        - The on-disk format and its versioning
        - Import and export of foreign formats
        - Autosave and crash recovery
---
name: rendering
description: "Domain: Turning a document into pixels."
---

# Rendering

## Covers

- The scene graph and its traversal
- Rasterisation and caching
- Colour management and profile conversion

## Excludes

- Anything that mutates the document. Rendering is read-only.

## Invariants

- The renderer is pure: same document, same pixels
- No rendering code imports from Tools

## Authority

This describes the product as it is meant to work. Where the implementation
disagrees, treat this as correct unless the task is explicitly to change it.

excludes is the property I would add first. A boundary stated only from the inside is half a boundary — and the exclusions are where the mistakes actually happen, because they are the plausible-looking wrong answers.

A backend platform

export domain Identity:
    description: Who a request is from, and what they may do.

    covers:
        - Authentication and session issuance
        - Roles, permissions and policy evaluation
        - Service-to-service credentials

    excludes:
        - User profile data. That is Accounts. Identity knows a subject id and nothing else about the person.

    entryPoints:
        - POST /auth/token
        - The `authorize(subject, action, resource)` policy call

export domain Billing:
    description: Charging for usage, and everything downstream of it.

    covers:
        - Metering and usage aggregation
        - Invoicing, payment capture and dunning
        - Plan and entitlement definitions

    excludes:
        - Enforcing entitlements at request time. Billing decides what a customer is entitled to; Identity enforces it.

    invariants:
        - Money is integer minor units. Never a float, anywhere, ever.
        - Every charge traces to an immutable usage record

skills/billing/SKILL.md:

---
name: billing
description: "Domain: Charging for usage, and everything downstream of it."
---

# Billing

## Covers

- Metering and usage aggregation
- Invoicing, payment capture and dunning
- Plan and entitlement definitions

## Excludes

- Enforcing entitlements at request time. Billing decides what a customer is
  entitled to; Identity enforces it.

## Invariants

- Money is integer minor units. Never a float, anywhere, ever.
- Every charge traces to an immutable usage record

## Authority

This describes the product as it is meant to work. Where the implementation
disagrees, treat this as correct unless the task is explicitly to change it.

Read those two excludes lines together. They are doing the thing domains exist for: drawing a line where two areas obviously touch, and saying which side each responsibility lands on. That is a decision you make once and then re-explain forever unless you write it down.

Extra properties that earn their place

PropertyUse it for
excludesthe near-misses, and where they actually belong
invariantsrules that hold across the whole area
ownerthe team or person, when there is one
entryPointsthe handful of API surfaces the outside world touches
dependenciesdomains this one is allowed to depend on

dependencies is worth a note. If your architecture has a layering rule between domains, this is where it goes — and it is exactly the kind of thing an agent violates by accident:

export domain Billing:
    dependencies:
        - Identity
        - Accounts

Linking components to their domain

Nothing built in does this, and you should add it. Extend component in your own framework module so every component must name the domain it lives in:

// src/framework.psy
import { Component as MorphicComponent, Domain } from "@psy/morphic"

export abstract spec Component extends MorphicComponent as component:
    abstract domain: Domain
export component LayerStack:
    description: The ordered stack of layers in a document.
    domain: Rendering

    responsibilities:
        - Maintain layer order and z-index
psy inspect LayerStack
LayerStack (src/components/LayerStack.psy)
  abstract: false
  keyword: component
  precedence: src/components/LayerStack.psy#LayerStack > src/framework.psy#Component

  description = "The ordered stack of layers in a document."
  domain = Rendering
  responsibilities = [
    "Maintain layer order and z-index"
  ]

domain = Rendering prints as a spec reference, not a string — that is the whole point. Name a feature by mistake and it is PSY4001; omit it and it is PSY3015. Your architecture map is now compiler checked, in three lines. See Specifying Software.

domain or component?

The one people get stuck on, from the domain side:

domaincomponent
Isa region of the problema thing in the codebase
You couldassign work to itpoint at it and open it
Containscomponentscode
Named likean area — Rendering, Billinga noun — LayerStack, InvoiceBuilder
How many3–7as many as have non-obvious behaviour

If a new hire would be assigned to it, it is a domain. If they would be told to go and read it, it is a component.

When not to write one

  • Per folder. Folders follow domains, not the other way round. A domain that exists because a directory exists is a directory listing.
  • For an area nobody works on. If no task is ever "in" it, covers has nobody to decide for.
  • As a layer. "Utils", "Helpers", "Core" and "Common" are not domains. They are where things go when the domains are wrong.

How it renders

---
name: rendering
description: "Domain: Turning a document into pixels."
---

# Rendering

## Covers

- The scene graph and its traversal
- Rasterisation and caching
- Colour management and profile conversion

## Excludes

- Anything that mutates the document. Rendering is read-only.

## Invariants

- The renderer is pure: same document, same pixels
- No rendering code imports from Tools

## Authority

This describes the product as it is meant to work. Where the implementation
disagrees, treat this as correct unless the task is explicitly to change it.

And the skill that puts it in front of an agent:

export skill ImplementRendering:
    description: Implement a change in the renderer.
    when: Use when changing anything under the rendering domain.

    context:
        - Rendering
        - LayerStack

    prompt:
        Keep mutation out of the renderer. If a change seems to require the
        renderer to write to the document, that is a design problem — say so
        rather than working around it.

Next: feature

On this page