The spec language · .elv

The floor above the code.

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.

The method

Sketch first. Deepen only where you worked.

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.

KeywordKindWhat it is
eExtensionOptional top tier — separable bundles: plugins, product variants, installable packs.
cCategoryA top-level grouping. Behaviourless: a name, a description, and a list of child Features. The onboarding ground floor.
fFeatureA cohesive user-facing capability. Noun-shaped: Protocol, Workflow, Conversation.
fuFunctionalityA verb on a Feature, and always a leaf. If it has several operations of its own, it is a Feature.
conceptConceptCross-cutting logic several Features pull in — tax calculation, LLM invocation. Flat, never nested.
uiUI PageA page in the product, targeted by where: edges. Auto-created on first reference.

Body fields

FieldMeaning
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.

ArtifactAnswersTime axisWhat rots it
ADRWhy we chose thisAppend-only, dated, superseded not editedNothing — but after two years nobody can tell which ADRs still govern code that still exists
Glossary (ubiquitous language)What the words meanLivingNothing ties a term to the code that implements it
C4 / arc42 diagramsWhat the boxes areSnapshot, redrawn by handThe redraw doesn't happen
ElevatorWhat the system does — and where that livesLiving, sketch-firstAuthored, so it can lag — but every anchor is machine-checked
The code graphHow it is actually wiredRecomputed on every saveNothing. 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.

call elevator { argv: "spec/ --drift --code-root ." } 8 lines
# 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.

call elevator { argv: "spec/ --drift --code-root ." } 5 lines
# 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 map
elevator ./spec --focus f.protocol # one branch, as an LLM context bundle
elevator ./spec --check            # broken refs, orphans — CI-friendly
elevator ./spec --code-map         # path → entities; surfaces double claims
elevator ./spec --drift --code-root .   # anchors that no longer hold
elevator --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.

How agents use it →