feature
A specific behaviour the product exhibits, described from outside.
export feature PenTool:
description: Draw and edit bezier paths.
behaviour:
Click places an anchor. Dragging while placing shapes the handles.
Clicking the first anchor closes the path.A feature is what the product does, described the way a user would
experience it.
It is the only definition kind written from outside the codebase looking in. Everything else — concepts, domains, components — describes structure. A feature describes behaviour, and specifically the behaviour somebody decided on rather than the behaviour that fell out.
behaviour — string, required
What happens, in terms of what the user does.
The discipline is: no implementation nouns. No classes, no functions, no services, no events. If a sentence mentions a component, it belongs in the component, not here.
# No. This is a component's responsibilities wearing a feature's hat.
behaviour:
PenTool registers with the ToolPalette and pushes AnchorCommands onto the
CommandStack, which the renderer picks up on the next frame.
# Yes. This is what somebody using the product would tell you happens.
behaviour:
Click places an anchor. Dragging while placing shapes the handles.
Clicking the first anchor closes the path.The second one is also the one that stays true when you rewrite the tool system.
Write the decided parts. If the behaviour is obvious from the name — "Zoom In makes things bigger" — you are padding, and padding in a normative document is worse than silence.
Which features to write
Write a feature when the behaviour was decided, not when it is obvious.
The signal is: there was a choice, and somebody made it, and a reasonable person would have made a different one. Undo coalescing. What happens on a failed export. Whether autosave overwrites. Those need definitions, because an agent implementing around them will otherwise guess — and guess plausibly, and guess wrong.
"Zoom in" does not need a definition. "Zoom in, and the zoom is anchored on the pointer rather than the canvas centre, because that is what people expect from maps" does.
Examples
A tool with edge cases
use "@psy/morphic"
export feature PenTool:
description: Draw and edit bezier paths.
behaviour:
Click places an anchor. Dragging while placing shapes the handles.
Clicking the first anchor closes the path.
Once a path is closed, further clicks start a new path rather than
editing the closed one. Editing requires selecting it first.
edgeCases:
- Escape abandons the in-progress path entirely. It is not undoable, because it was never committed.
- A path with a single anchor is discarded on deselect rather than left as a stray point.
- Dragging with no prior click does nothing. It is not a drag-to-draw gesture.
constraints:
- Every completed path is one undo step, not one per anchor---
name: pen-tool
description: "Feature: Draw and edit bezier paths."
---
# PenTool
## Behaviour
Click places an anchor. Dragging while placing shapes the handles. Clicking the
first anchor closes the path.
Once a path is closed, further clicks start a new path rather than editing the
closed one. Editing requires selecting it first.
## Edge Cases
- Escape abandons the in-progress path entirely. It is not undoable, because it
was never committed.
- A path with a single anchor is discarded on deselect rather than left as a
stray point.
- Dragging with no prior click does nothing. It is not a drag-to-draw gesture.
## Constraints
- Every completed path is one undo step, not one per anchor
## 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.edgeCases is where most of a feature's value lives. The main behaviour is
usually guessable; the edges never are, and they are exactly what gets
implemented wrong.
A feature with states
export feature Autosave:
description: Preserve unsaved work without the user asking.
behaviour:
Five seconds after the last edit, the document is written to a
recovery file beside the original. The recovery file is deleted on a
successful explicit save, and on a clean quit.
The user is never told this is happening. If it fails, they are not
told that either — a failed autosave is logged and retried, because an
alert about a background operation the user did not request is worse
than the risk it is warning about.
states:
- Clean — no recovery file, document matches disk
- Dirty — edits since last save, recovery file current
- Recovering — a recovery file was found at launch
constraints:
- Autosave never writes to the user's file. Only the recovery file.
- Recovery is offered once. Declining it deletes the file.Note the second paragraph of behaviour. It states a decision and its reason,
which is what stops somebody helpfully adding an error toast six months later.
A feature family, by inheritance
Features are ordinary specs, so shared behaviour is written once:
export feature DrawingTool:
description: Behaviour common to every drawing tool.
behaviour:
Activating a tool deactivates the previous one and commits any
in-progress work. Escape always cancels without committing.
constraints:
- One completed gesture is one undo step
export feature PencilTool extends DrawingTool:
description: Draw freehand strokes.
behaviour:
+ super
Dragging draws a stroke that follows the pointer. Stroke width follows
pressure where the input device reports it.
export feature EraserTool extends DrawingTool:
description: Remove parts of a stroke.
behaviour:
+ super
Dragging erases within the current layer only. It never erases across
layers, even where they overlap visually.---
name: pencil-tool
description: "Feature: Draw freehand strokes."
---
# PencilTool
## Behaviour
Activating a tool deactivates the previous one and commits any in-progress work.
Escape always cancels without committing.
Dragging draws a stroke that follows the pointer. Stroke width follows pressure
where the input device reports it.
## Constraints
- One completed gesture is one undo step
## 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.PencilTool.behaviour is both paragraphs, joined by a blank line, and its
constraints are inherited whole. Change the cancellation rule once and every
tool's generated artifact updates. See Composing.
A backend feature
Features are not only for user interfaces. Anything with externally-observable behaviour qualifies:
export feature UsageMetering:
description: Count what a customer used, for billing.
behaviour:
Every billable action emits a usage record with a customer, a meter, a
quantity and an idempotency key. Records are aggregated hourly.
Late-arriving records are accepted for 48 hours and land in the period
they were emitted in, not the period they arrived in.
constraints:
- A duplicate idempotency key is dropped silently, not an error
- Usage records are immutable once written
edgeCases:
- A record arriving after its period is invoiced becomes an adjustment on the next invoice. Invoices are never reissued.Extra properties that earn their place
| Property | Use it for |
|---|---|
edgeCases | the decided behaviour at the boundaries |
constraints | rules the behaviour must not violate |
states | when the feature is a state machine and the states have names |
acceptance | how you would know it works |
formats, limits, … | whatever this specific feature has |
feature or component?
They frequently describe the same code from two sides, and that is fine — you often want both.
| feature | component | |
|---|---|---|
| Point of view | outside, a user | inside, a maintainer |
| Says | what happens | what it is responsible for |
| Stays true when | the implementation is rewritten | the behaviour is tweaked |
| Named like | an action or capability | a noun |
Export the feature says a user picks a format and a path and gets a file.
ExportPipeline the component says it owns format negotiation and never mutates
the document. Different readers, different failure modes, both worth having.
When not to write one
- For obvious behaviour. Padding in a normative document costs more than it gives.
- For unbuilt behaviour. A definition is normative — write a feature for something you intend to build and an agent will treat it as true and "fix" the code to match. If it is not built and not being built now, it is an issue.
- As a spec document. A feature is the decided shape, not a full acceptance suite. If it is longer than a screen, some of it is edge cases and some of it belongs in tests.
How it renders
---
name: pen-tool
description: "Feature: Draw and edit bezier paths."
---
# PenTool
## Behaviour
Click places an anchor. Dragging while placing shapes the handles. Clicking the
first anchor closes the path.
## Edge Cases
- Escape abandons the in-progress path entirely. It is not undoable, because it
was never committed.
- A path with a single anchor is discarded on deselect rather than left as a
stray point.
## Constraints
- Every completed path is one undo step, not one per anchor
## 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.Next: component