Skip to content

The authoring rule

Mateu has a large declarative surface. It has stayed learnable because of one rule that has governed it from the start — and this page writes it down, because a rule nobody can read is a rule only the maintainer can apply.

If you are extending Mateu, this decides where your new capability belongs. If you are only using it, it explains why the API looks the way it does, and lets you guess right about parts you have not read yet.

The first splits by when the answer is known:

Annotation — purely declarative: known at compile time, no decision to make. Interface (supplier / abstract method) — there is a decision, and it is taken at runtime.

That is why a page’s subtitle is @Subtitle("…") and SubtitleSupplier.subtitle(): one is a constant, the other is computed. The same pair appears for the title, the overline, the title placeholder, peer navigation, page width. When both are present, the supplier wins — a runtime value outranks a declared one.

The second question splits by who decides:

you decideMateu decides
compile timeannotationinference from the shape of your model
runtimeinterface / abstract methodinference + composition

The left-hand column is the rule above. The right-hand column is layout inference and page inference: Mateu reading the information you declared and choosing the presentation you did not.

This is why @AutoPage feels unlike every other annotation: its content is not information, it is permission — “decide this for me”. It is not a declaration, it is a change of column.

From those two axes comes the rule that decides where a new capability goes:

An annotation is legitimate when it carries information that only you have. It is debt when it carries a decision Mateu could take by looking at the model.

A @Timestamp field marks which of your fields is the last-updated one — Mateu cannot know that; information only you have. A two-column layout for a form of six short fields is not information: it is a decision derivable from the fields, so it is inferred, and the annotation exists only to override.

Applied consistently, the surface shrinks as inference improves instead of accumulating.

The same partition governs delivery, which is why it is worth getting right:

AuthoringDelivery
annotation — compile time, no decisioncan be pre-rendered into a static bundle and served from a CDN
interface — decision at runtimeneeds the server

Both ask the same thing: can this be known without the request? So the rule of thumb for “will this screen work with no backend” is not a list of exceptions to memorise — it is if you declared it with annotations, it can ship statically; if you used an interface, it needs a server.

The rule above decides case by case. The risk it cannot catch is accumulation: every annotation is justifiable on its own, and the sum still drifts the framework away from the reason it exists.

So it is counted. scripts/annotation-density.py measures how many Mateu annotations a screen needs across the demo and e2e corpus — 4.38 per screen over 444 screens as of 2026-08-12 — and CI fails if the average creeps above a recorded ceiling.

The ceiling carries deliberate headroom (~5%): pinned to the exact current value it would fail on the very next annotation added anywhere, and a check that fires on ordinary work gets raised by reflex until it means nothing. Raising it is allowed. Raising it silently is the thing the number exists to prevent.

Inference has a ceiling, and it is worth knowing where it is so you do not wait for a version that lifts it. See the limits of inference.