A code graph can tell you everything about how a system is wired and nothing about what it
is for. Elevator is the smallest language we could design for that layer — and it is built
so that leaving it unfinished is a legitimate, permanent state.
Nothing is required beyond a name. That is deliberate: a spec that demands completeness
gets filled in once, by whoever was told to, and is wrong within a quarter.
Sketch new areas in one line. The day you learn an area exists, add c name { d: "one sentence" }. A body-less definition is complete
and valid — breadth without claiming depth it does not have.
Deepen the branch you just touched. After implementing something, add its
Feature and verbs then — the knowledge is cheap the day you have it and
expensive to reconstruct later.
Don't deepen what you didn't touch. No sequence diagrams, no state machines, no
actors. That detail belongs in the code or an ADR.
Let the tools be the radar.--check errors must be fixed; its hints
are a to-deepen list, not a to-fix list.
Depth where work happens is also the depth that stays fresh. Everywhere else, one honest
line beats a paragraph nobody verified.
spec/library.elv
import"concepts.elv"c library {
d:"Reusable building blocks users assemble into runs."f protocol, f template, f chain
}
f protocol {
d:"Structured prompt with an enforced step sequence."cr:"src/protocol/"fu creation, fu edition
}
fu f.protocol.creation {
d:"Builder validates step order before persist; rejects empty steps."cr:"src/protocol/builder.rs", "src/protocol/validator.rs"where: ui.library.protocol
references: f.conversation
}
concept llm_invocation {
d:"Every model call goes through the retry + budget wrapper."used_by: f.protocol, f.template
}
Syntax
Six entity kinds. Every definition flat, every hierarchy by reference.
Definitions never nest. A body holds metadata and uses of other entities by name — f protocol { fu creation } asserts containment between two entities
that are each defined at the top level.
A top-level grouping. Behaviourless: a name, a description, and a list of child Features. The onboarding ground floor.
f
Feature
A cohesive user-facing capability. Noun-shaped: Protocol, Workflow, Conversation.
fu
Functionality
A verb on a Feature, and always a leaf. If it has several operations of its own, it is a Feature.
concept
Concept
Cross-cutting logic several Features pull in — tax calculation, LLM invocation. Flat, never nested.
ui
UI Page
A page in the product, targeted by where: edges. Auto-created on first reference.
Body fields
Field
Meaning
d:
One sentence of description. Put the payload here: config keys, cron expressions, the one trap.
cr:
Code reference — the file or folder implementing this entity. Comma-separated; cr.fe: / cr.be: partition layers.
where:
This Feature or Functionality appears on that UI Page.
references:
A cross-link to another Feature that the hierarchy does not capture.
used_by:
On a Concept: which Features pull it in.
Strict, explicit imports
A file can only reference what it defines or what it explicitly imports, transitively. No ambient cross-file resolution — every cross-file dependency is a
written contract. Out-of-scope references stay visible in the graph, tagged, with a
warning naming the missing import.
Broken links stay visible
A reference to something defined nowhere gets a synthetic stub entity tagged unresolved, so the break shows up in the map instead of being silently
dropped. Unfinished is allowed; invisible is not.
Rendered
nao's own spec, as a graph.
Amber is a Feature, deep purple a Category, light amber a Functionality, cyan a Concept —
the same colours the VS Code extension uses. This is the map a newcomer reads before
opening a single source file.
analysing…
Not a replacement
Where it sits next to the documents you already keep.
Nothing here is new in spirit. Architecture Decision Records, a ubiquitous-language
glossary, a C4 diagram, docs-as-code — all of them are attempts at the same thing, and each
answers a different question. Elevator is deliberately narrow: it takes the one question the
others keep gesturing at and can't hold for long.
Artifact
Answers
Time axis
What rots it
ADR
Why we chose this
Append-only, dated, superseded not edited
Nothing — but after two years nobody can tell which ADRs still govern code that still exists
Glossary (ubiquitous language)
What the words mean
Living
Nothing ties a term to the code that implements it
C4 / arc42 diagrams
What the boxes are
Snapshot, redrawn by hand
The redraw doesn't happen
Elevator
What the system does — and where that lives
Living, sketch-first
Authored, so it can lag — but every anchor is machine-checked
The code graph
How it is actually wired
Recomputed on every save
Nothing. It is derived, not written
ADRs and Elevator fix each other's weakness
An ADR is a decision frozen at a moment: excellent history, and silent about the present.
Read 0001-per-language-parsers-stay-independent today and you still don't
know which parsers exist, or whether the rule survived.
Elevator is the opposite: a standing description of what exists now, with no memory of
why. Point one at the other and both get what they lack — why from the record, what and where from the spec, and how from the graph underneath.
C4's instinct, a different axis
C4 is right that abstraction needs levels. It slices by system — context,
container, component, code. Elevator slices by capability — category, feature,
the verbs under it.
It also stops earlier on purpose. C4's bottom two levels describe components and code,
which is exactly the layer nao computes from source. Drawing by hand what a parser can
derive is the part that always goes stale first.
The join
Make the decision link fail the build.
Code references take a free-form tag — cr.fe:, cr.be:, cr.test:. Nothing stops you using cr.adr:, and the moment you do,
the link between a decision and the code it governs becomes a checked anchor rather than a
sentence someone hopes is still true.
spec/code_graph.elv
f parsers {
d:"One parser per language behind the LanguageParser trait."cr:"src/parser/"
cr.adr: "docs/adr/0001-per-language-parsers-stay-independent.md"
}
--drift then verifies that path resolves like any other. Delete or rename the
ADR without touching the spec and the report errors, with exit code 1 — so CI catches a
dangling decision the same way it catches a dangling import.
Honest scope: cr.adr is a convention, not a parser feature — the tag
vocabulary is free-form and Elevator attaches no meaning to it. What is mechanised is the
path check. Nothing verifies the ADR's content still matches the code, and Elevator
does not replace the immutability and supersession an ADR log gives you.
# Elevator drift report
> code root: .
> 3 cr reference(s): 2 resolved, 1 missing — 1 identifier(s) checked in 95 file(s), 0 unanchored
## Missing cr paths (error)
> The code moved or was removed — fix the `cr:` or retire the entity.
f parsers
cr: docs/adr/0007-a-decision-that-was-deleted.md — not found under code root
Real run: a spec claiming an ADR that no longer exists. Exit code 1 — the same signal a failing test gives CI.
# Elevator drift report
> code root: .
> 29 cr reference(s): 21 resolved, 0 missing — 15 identifier(s) checked in 284 file(s), 0 unanchored
✓ No drift detected: every cr path resolves and every identifier in `d:` is anchored in the claimed code.
And the same command against nao's own spec, where every anchor still holds.
Tooling
The spec is checkable, which is the whole point.
Descriptions are authored, so this is the one layer that can lag the code. Elevator's
answer is to make every anchor verifiable by a machine.
--check — broken references and orphans. Errors fail; hints are a to-deepen list.
--code-map — path → entities, which surfaces the drift this abstraction is prone to: the same code claimed by two differently-named entities.
--drift — every cr: path still resolves, and every identifier named in a d: still appears in the file it claims. The rename-and-delete detector.
--focus — one branch as a compact context bundle, for feeding an LLM the part of the map it needs.
elevator
elevator ./spec # the full project mapelevator ./spec --focus f.protocol # one branch, as an LLM context bundleelevator ./spec --check# broken refs, orphans — CI-friendlyelevator ./spec --code-map# path → entities; surfaces double claimselevator ./spec --drift--code-root . # anchors that no longer holdelevator--docs# the full language reference
And over MCP
The overview tool serves the same map — or a focused bundle — straight to
an agent. It is the grounding step before the code map: which Feature does this task
belong to, and which files does that Feature claim.