Step 4: The Components
Four things in the codebase, what each is responsible for, and what must stay true.
Now the things you could point at and open. Four of them, and we are going to make the compiler check that each one declares which domain it lives in.
First, require a domain
Extend the component keyword before writing any. src/framework.psy:
import { Component as MorphicComponent, Domain } from "@psy/morphic"
export abstract spec Component extends MorphicComponent as component:
abstract domain: Domain
testing:
Every component has a unit test for each of its invariants. A component
that touches the bitmap also has a test that renders a known stroke and
compares pixels.Two things just happened:
- Every component must now declare
domain, and it must be a realdomain— a typed reference, not a string. - Every component inherits a
testingsection, written once.
Modules now use "./framework" instead of use "@psy/morphic" for components.
See making the vocabulary yours.
CanvasSurface
src/components/CanvasSurface.psy:
use "../framework"
import { Canvas } from "../domains"
export component CanvasSurface:
description: Owns the pixel buffer and everything written into it.
domain: Canvas
responsibilities:
- Own the pixel buffer and its fixed dimensions
- Composite a stamp onto the buffer at a position
- Snapshot and restore the buffer for undo
- Never decide what to stamp. It is told a shape and a colour.
invariants:
- The buffer is allocated once per drawing and never resized
- A stamp outside the bounds is clipped, never an error
- Snapshots are taken before a stroke begins, not during it.claude/skills/canvas-surface/SKILL.md:
---
name: canvas-surface
description: "Component: Owns the pixel buffer and everything written into it."
---
# CanvasSurface
## Responsibilities
- Own the pixel buffer and its fixed dimensions
- Composite a stamp onto the buffer at a position
- Snapshot and restore the buffer for undo
- Never decide what to stamp. It is told a shape and a colour.
## Invariants
- The buffer is allocated once per drawing and never resized
- A stamp outside the bounds is clipped, never an error
- Snapshots are taken before a stroke begins, not during it
## Testing
Every component has a unit test for each of its invariants. A component that
touches the bitmap also has a test that renders a known stroke and compares
pixels.
## 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.Note the fourth responsibility. "Never decide what to stamp" is a negative responsibility, and it is worth more than the three above it — the plausible wrong move is to give the thing that owns the pixels some opinions about brushes, and this rules it out at the point somebody would look.
Note also that domain: Canvas produced no section. Reserved names and typed
references are not rendered as content; only strings and string lists become
sections. See
what does and does not become one.
ToolPalette
src/components/ToolPalette.psy:
use "../framework"
import { Tools } from "../domains"
export component ToolPalette:
description: Which tool is active, and the options for it.
domain: Tools
responsibilities:
- Hold the set of available tools and which one is active
- Hold the current options for each tool independently
- Produce the stamp for the active tool on request
invariants:
- Exactly one tool is active. There is no null state.
- Each tool remembers its own options. Changing brush size does not
change eraser size.That second invariant is the sort of thing that is obvious to a user, invisible in code, and wrong in roughly half of all first implementations.
ColorPicker
src/components/ColorPicker.psy:
use "../framework"
import { Tools } from "../domains"
export component ColorPicker:
description: The foreground and background colours.
domain: Tools
responsibilities:
- Hold the foreground and background colour
- Present the palette and the current pair
- Emit a colour change; never apply one itself
invariants:
- Colours are opaque RGB. There is no alpha channel anywhere.Three lines, one of them negative, and the invariant ties straight back to the concept — a drawing has no transparency, so a colour cannot have one either. That consistency is not an accident; it is what having a concept gets you.
FileStore
src/components/FileStore.psy:
use "../framework"
import { Files } from "../domains"
export component FileStore:
description: Reading and writing PNG files.
domain: Files
responsibilities:
- Encode the buffer as PNG and write it to a path
- Decode a PNG into a buffer
- Track the current file path and the unsaved-changes flag
- Never prompt. It reports what it needs; the UI asks.
invariants:
- Writes go to a temporary path and are moved into place
- A decode failure leaves the current drawing untouched.claude/skills/file-store/SKILL.md:
---
name: file-store
description: "Component: Reading and writing PNG files."
---
# FileStore
## Responsibilities
- Encode the buffer as PNG and write it to a path
- Decode a PNG into a buffer
- Track the current file path and the unsaved-changes flag
- Never prompt. It reports what it needs; the UI asks.
## Invariants
- Writes go to a temporary path and are moved into place
- A decode failure leaves the current drawing untouched
## Testing
Every component has a unit test for each of its invariants. A component that
touches the bitmap also has a test that renders a known stroke and compares
pixels.
## 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 contract earning its keep
Try leaving out the domain:
export component Toolbar:
description: The row of buttons along the top.
responsibilities:
- Show the available toolssrc/components/Toolbar.psy:5:1 error PSY3015: `Toolbar` does not define abstract
property `domain`.
src/framework.psy:4:5: `domain` is required hereOr naming the wrong kind of thing:
export component Toolbar:
description: The row of buttons along the top.
domain: Pencilsrc/components/Toolbar.psy:3:13 error PSY4001: `Toolbar.domain` must be `Domain`
but `Pencil` is a Feature.
src/framework.psy:4:5: `domain` is declared hereSix lines in src/framework.psy bought a compiler-checked architecture map. Every
component is placed, every placement is real, and nobody can add one that floats.
Check
psy checkcheck: no problems found in 17 file(s)Next: The References