Morphic Agent Framework

component

A reusable unit of implementation — what it is responsible for, and what must stay true about it.

export component LayerStack:
    description: The ordered stack of layers in a document.

    responsibilities:
        - Maintain layer order and z-index
        - Apply blend modes and opacity

    invariants:
        - Order is stable across save and load
        - The renderer never mutates it

A component is a thing in the codebase you could point at. A class, a module, a service, a package — whatever your language calls a unit that has a name and a job.

It is the most numerous definition kind, and the one where the temptation to write documentation is strongest. Resist it.

responsibilitiesstring[], required

The jobs, not the API.

A responsibility is something the component is accountable for. It survives a refactor, a rename, and a change of language. A method list survives none of those and is wrong the first time somebody adds an overload.

# No. This is an API listing, and it is already out of date.
responsibilities:
    - push(layer)
    - remove(id)
    - reorder(from, to)
    - flatten()

# Yes. This is what it is for.
responsibilities:
    - Maintain layer order and z-index
    - Apply blend modes and opacity
    - Produce a flattened view for the renderer without mutating itself

Start each with a verb. "Maintain", "Own", "Decide", "Translate", "Enforce". If a responsibility does not start with a verb it is probably a noun phrase describing the code rather than the job.

Negative responsibilities are the best ones

    responsibilities:
        - Maintain layer order and z-index
        - Produce a flattened view for the renderer
        - Never persist. Serialisation belongs to DocumentStore.

That last line is worth ten of the others. Agents do not violate the things you listed; they violate the things you did not think to rule out, and the plausible wrong move is almost always "add persistence to the thing that has the data".

Anywhere you have ever said "no, that doesn't go there" in a review, there is a negative responsibility waiting to be written.

invariants — the one to add first

Not required. Add it anyway, on anything that has one.

An invariant is something that must remain true, that a plausible change could break. It is the highest-signal line you can write about a component, because it is precisely the class of thing an agent cannot infer from the surrounding code — the code shows what is true, not what must be.

export component CommandStack:
    description: The undo/redo history for a document.

    responsibilities:
        - Record every document mutation as a reversible command
        - Coalesce rapid mutations from a single gesture into one entry

    invariants:
        - A mutation that bypasses this is a bug, not an optimisation
        - Undo is total: every recorded command can be reversed exactly
        - The stack is cleared on document close, never trimmed on size
---
name: command-stack
description: "Component: The undo/redo history for a document."
---

# CommandStack

## Responsibilities

- Record every document mutation as a reversible command
- Coalesce rapid mutations from a single gesture into one entry

## Invariants

- A mutation that bypasses this is a bug, not an optimisation
- Undo is total: every recorded command can be reversed exactly
- The stack is cleared on document close, never trimmed on size

## 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.

"A mutation that bypasses this is a bug, not an optimisation" is a sentence I have said out loud in review more than once. Written down, with authority behind it, it is no longer my job to say it.

Examples

With a domain and an owner

Extending component in your own framework so every one must declare where it lives and who owns it:

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

export abstract spec Component extends MorphicComponent as component:
    abstract domain: Domain
    abstract owner: string

    testing:
        Every component ships with a unit test for its invariants and an
        integration test for its collaborators.
// src/components/DocumentStore.psy
use "../framework"

import { Files } from "../domains"

export component DocumentStore:
    description: Reading and writing documents to disk.
    domain: Files
    owner: platform-team

    responsibilities:
        - Own the on-disk format and its version migrations
        - Translate between the document model and the serialised form
        - Never interpret document content: it moves bytes, not meaning

    invariants:
        - A document written by version N is readable by version N+1
        - A failed write never leaves a partial file on disk

    collaborators:
        - LayerStack — read only, for serialisation

domain: Files is a typed reference. Name a feature by mistake and the build fails with PSY4001; omit it and PSY3015. Your architecture map is now compiler-checked. See Specifying Software.

Every component also inherits the Testing section, from one edit.

A service

export component InvoiceBuilder:
    description: Turns a billing period's usage into an invoice.
    domain: Billing
    owner: billing-team

    responsibilities:
        - Aggregate usage records into invoice line items
        - Apply plan pricing, discounts and tax in that order
        - Emit an immutable invoice document

    invariants:
        - Money is integer minor units throughout. No floats, ever.
        - Building the same period twice produces an identical invoice
        - An invoice is never modified after emission; corrections are adjustments on the next one

    dependencies:
        - UsageStore
        - PlanCatalogue
---
name: invoice-builder
description: "Component: Turns a billing period's usage into an invoice."
---

# InvoiceBuilder

## Responsibilities

- Aggregate usage records into invoice line items
- Apply plan pricing, discounts and tax in that order
- Emit an immutable invoice document

## Invariants

- Money is integer minor units throughout. No floats, ever.
- Building the same period twice produces an identical invoice
- An invoice is never modified after emission; corrections are adjustments on
  the next one

## Dependencies

- UsageStore
- PlanCatalogue

## 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.

The determinism invariant is the kind of thing that reads as pedantic right up until somebody introduces a Date.now() into a pricing path.

A small one

Not every component needs paragraphs:

export component ColorPicker:
    description: Choose a colour.
    domain: Tools
    owner: ui-team

    responsibilities:
        - Present the colour space and current selection
        - Emit a colour change; never apply one itself
---
name: color-picker
description: "Component: Choose a colour."
---

# ColorPicker

## Responsibilities

- Present the colour space and current selection
- Emit a colour change; never apply one itself

## Testing

Every component ships with a rendering test and an interaction test.

## 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.

Two lines, one of them negative, and it has said the only non-obvious thing about the component. (domain and owner are typed references and a reserved-adjacent scalar, so neither renders as a section — only strings and string lists do.)

Extra properties that earn their place

PropertyUse it for
invariantswhat must stay true — write these first
domainwhich domain it lives in, as a typed reference
ownerthe team, when there is one
collaboratorswhat it talks to, and in which direction
dependencieswhat it is allowed to depend on
testinghow this kind of component gets tested — usually inherited

collaborators and dependencies are easy to overdo. Write them when the relationship is constrained ("read only", "events only", "never the reverse"), not to draw a graph — the graph is in the imports, and it is more accurate there.

Which components to write

When the behaviour is not obvious from the name, or when it has an invariant somebody could break.

Button needs no definition. CommandStack does. ColorPicker needed exactly two lines because it had exactly one non-obvious rule.

If you cannot think of a responsibility that is not implied by the name, and it has no invariants, skip it. A definition that restates the name is noise, and it is normative noise, which is worse.

component or domain?

From the component side:

componentdomain
Isa thing in the codebasea region of the problem
You couldopen itbe assigned to it
Named likea noun — LayerStackan area — Rendering
Belongs toone domainthe concept

If you would tell a new hire to "go and read it", it is a component. If you would tell them they are "working on it this sprint", it is a domain.

How it renders

---
name: command-stack
description: "Component: The undo/redo history for a document."
---

# CommandStack

## Responsibilities

- Record every document mutation as a reversible command
- Coalesce rapid mutations from a single gesture into one entry

## Invariants

- A mutation that bypasses this is a bug, not an optimisation
- Undo is total: every recorded command can be reversed exactly
- The stack is cleared on document close, never trimmed on size

## Testing

Every component ships with a unit test for its invariants and an integration
test for its collaborators.

## 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 hands it over:

export skill ImplementTool:
    description: Add or change a drawing tool.
    when: Use when changing anything under the tools domain.

    context:
        - Tools
        - CommandStack
        - ColorPicker

    prompt:
        Every completed gesture is one undo step. Route every mutation through
        the command stack.

Next: reference

On this page