Prose Design: Why Hand-Written?
Prose templates render operations as human-readable BDD steps in test output:
Scenario: Create and complete a todo
✓ Taylor creates a todo "Buy milk" (1.8ms)
✓ Taylor completes the todo "Buy milk" (1.2ms)
Template Syntax
Prose templates are plain strings with interpolation:
| Syntax | Purpose | Example |
|---|---|---|
{param} |
Operation parameter | '{actor} creates a todo "{title}"' |
{actor} |
Scenario actor name | '{actor} lists todos' |
[param? text] |
Conditional (shown if truthy) | '{actor} lists todos [includeCompleted?including completed]' |
{$binding.field} |
Value from prior step | '{actor} completes the todo "{$todo.title}"' |
Why Hand-Written
Structure is derivable from the schema. Given an operation createUser(name, email, role), a generator could emit:
"{actor} calls createUser with name {name}, email {email}, role {role}"
But this is robotic. The English phrasing carries semantic intent that the algebra deliberately does not prescribe:
createUsercould be “creates a user”, “registers an account”, or “signs up”deletePostcould be “deletes the post”, “removes the article”, or “unpublishes”- Conditional blocks like
[includeCompleted?including completed]require knowing which parameters are flags vs. data
The operation algebra defines what can happen. Prose defines how we talk about it. These are separate concerns: one is structural, the other is presentational. Conflating them would force domain language into the schema or force generic phrasing onto users.
Algebraic Role
In the categorical model (see algebraic-foundations.md), the schema defines a free algebra F_dsl — the initial object in the category of T-algebras. Every concrete interpretation (handlers, API routes, CLI commands, tests) is a unique homomorphism from F_dsl.
Prose is one such interpretation: a projection from F_dsl to natural language. Initiality guarantees that a mechanical projection exists — you can always derive "{actor} calls {opName} with {params}". But initiality says nothing about the quality of that projection. Hand-written prose is a semantically rich interpretation that preserves domain meaning where a mechanical one would lose it.
This is the same reason handler implementations are hand-written: the algebra tells you the signature, but the business logic requires domain knowledge.
Where Prose Lives
Prose follows the same fixture pattern as handler implementations:
impls/src/prose.ts Hand-written fixture
↓ re-exported
core/src/index.ts Public API (import { prose } from "@app/context-core")
↓ imported by
scenario runner Interpolates templates during test execution
For morph’s own contexts: extensions/<ext>/impls/src/prose.ts
For example apps: examples/fixtures/<app>/dsl/prose.ts
Interpolation Flow
- Fixture defines templates:
createTodo: '{actor} creates a todo "{title}"' - Runner receives prose in config:
createLibraryRunner({ prose, ... }) - Execution resolves bindings (
$todo.title→ actual value from prior step) renderStepProse()interpolates{actor},{params},[conditionals],{$bindings}- Output prints rendered step:
✓ Taylor creates a todo "Buy milk" (1.8ms)
If no prose template exists for an operation, the runner falls back to a structural representation: operationName(param=value, ...).
Multi-Context Composition
When an app has multiple contexts (e.g., blog with users + posts), each context provides its own prose. The generator merges them into a single flat Record<string, string> keyed by operation name (which is unique across contexts by schema constraint). At runtime, renderStepProse() looks up by operation name — no merging algorithm needed.
Auto-Generation as Complement
The Auto-generate prose templates backlog item (P3/M) would generate default prose as a starting point — not a replacement for hand-written prose. Users could accept the defaults for rapid prototyping and refine them later. This preserves the design: prose is always a fixture, whether initially generated or written from scratch.