Most systems drift. Rules creep into command handlers. A fuzzy matcher written for one command turns into a chunk of logic nobody else can use. Business rules and plumbing become indistinguishable.
The alternative is older than code: declare what exists, let the system read the declaration.
The four parts
A declarative core has four moving parts:
- A registry — YAML or JSON. Lists what exists.
- A resolver — matches input to a registry entry.
- A handler — does the work the entry describes.
- A router — pipes input through resolver into handler.
No business logic in the router. No matching logic in the handler. Each part has one job.
A registry
A registry is just data. It lists commands and the patterns that match them.
commands:
- id: note.create
patterns: ["new note", "create note", "jot"]
handler: notes.create
- id: note.search
patterns: ["find note", "search notes"]
handler: notes.search
Adding a command means adding an entry. Nothing else changes.
A resolver
The resolver does not know what commands mean. It knows how to match input against patterns.
def resolve(text: str, registry: list[dict]) -> dict:
"""Return the matching registry entry. Raise if nothing matches."""
lowered = text.lower()
for entry in registry:
for pattern in entry["patterns"]:
if pattern in lowered:
return entry
raise NoMatch(text)
Swap this for fuzzy matching, a small neural network, a tiny on-device LLM. The contract stays: input in, registry entry out.
A fuzzy matcher is not code inside a command handler. It is a capability with a defined interface that the system uses through a contract.
A handler
The handler does not know how the match was made. It receives a context and does the work.
def notes_create(context: dict) -> Result:
payload = context["payload"]
note = Note.from_text(payload["text"])
note.save()
return Result.ok(note.id)
Handlers never reference each other. They only touch the context they are given.
A router
The router is the pipe. Three lines.
def route(text: str) -> Result:
entry = resolve(text, registry)
handler = handlers[entry["handler"]]
return handler(context_for(text))
No conditionals by command name. No special cases. If it belongs in the router, it applies to every command.
Why this holds
The shape of the code is the enforcement.
- You cannot add a command without adding a registry entry.
- You cannot change matching without changing the resolver.
- You cannot couple handlers because handlers only receive context.
Every piece is replaceable. The router stays stupid. Matching lives in the resolver. Work lives in the handler. Contracts at every seam.
That is the whole trick.