← All articles

The Gap Between 'Agreed To' and 'Verified': Building LeanAgileScript

In my last post, I toured the VS Codium extension suite and the shared platform layer underneath it. This post is narrower and, honestly, less finished — which is the point. It's about a language extension called LeanAgileScript, why I started building it, and where it actually stands today versus where the plan says it should be.

The Gap

Most specification documents I've worked with — requirements docs, acceptance criteria, "definition of done" checklists — live in a place the test suite can't see. Someone writes "the user must be able to reset their password within 3 attempts," a reviewer reads it, everyone nods, and then the actual verification of that rule happens somewhere else entirely: in a developer's head while they write the implementation, or in a QA engineer's head while they write a test, translated by hand each time with no guarantee the translation was faithful.

That gap — between what's "agreed to" in a document and what's actually verified by running code — is where most of the disagreements I've seen late in a project come from. Not disagreements about whether the feature works, but disagreements about what "works" was supposed to mean in the first place.

LeanAgileScript is my attempt at closing that gap: a small, structured, machine-readable language for writing acceptance criteria and domain models, so the specification itself can be parsed, validated, and — eventually — used to generate the scaffolding that keeps an implementation honest to what was agreed.

What It Is

LeanAgileScript is a domain-specific language (DSL) built with Langium, an open-source language engineering framework. Instead of writing acceptance criteria in prose or in a Gherkin-style Given/When/Then format, you write structured definitions — modules, data types, object types, and their constraints — in a syntax that's still readable in plain English but is formally parseable.

Here's a real excerpt from the one worked example in the repo, an e-commerce module definition:

Define E-Commerce as a Module with:
  Data-Types:
    Define Latitude as a Decimal Data-Type with:
      Constraints:
        Min -90
        Max 90

That's not decoration — Min -90 and Max 90 are real constraints the language server understands, not just prose a human has to remember to enforce elsewhere. Object types work the same way: you Define <Name> as a <Type> with: an Attributes: block, and each attribute can carry its own Constraints:. Collections are declared as <Type> List, <Type> Set, or <KeyType> to <ValueType> Map. The built-in primitive types cover the usual ground — Flag, Integer, Decimal, Text, Unique-ID, Sortable-ID, Timestamp, and a few string-shape primitives like Letter, Digit, and Alpha-Numeric.

The Build

The extension is three Nx projects under extensions/studios/lean-agile-studio/script/:

  • packages/language — the actual DSL: a Langium grammar (.langium files), the generated parser and AST, and language services (validation, and eventually formatting and completion)
  • packages/extension — the VS Code language client, which is deliberately thin
  • packages/cli — a command-line entry point for parsing .lascript files outside the editor

Langium handles the heavy lifting most people associate with building a language from scratch: you write a grammar, and Langium's code generator (langium-cli) produces the parser, the AST types, and the dependency-injection wiring that ties them to the Language Server Protocol. Under the hood it's using Chevrotain as the actual parsing engine — you don't touch that directly, but it's worth knowing it's there if you ever go spelunking in the generated output. The generated files carry a "DO NOT EDIT MANUALLY" header for a reason; if you find yourself wanting to hand-edit generated/ast.ts, that's a signal you need to change the grammar instead.

The extension host side is almost embarrassingly simple by design. extension/main.ts spins up a LanguageClient pointed at a separate language-server process; language/main.ts — that server process — creates an LSP connection, builds the Langium services, and calls startLanguageServer(). Fourteen lines. No business logic, no data access, nothing but a Langium bootstrap. That simplicity isn't an accident — it's an architectural boundary I drew on purpose, and I broke it once before I drew it.

The Honest Limitation

Here's the part of this post I could have skipped and you'd never have known. I'm not going to skip it.

Early in this extension's life, I wired a real application service — FounderOrchestrationService, which coordinates founder-facing project orchestration in the Lean-Agile Studio product — directly into the extension's activation code. It worked, in the sense that the code ran. But it meant business logic, repository access, and event-bus subscriptions were now living inside a Langium workspace, coupled to the grammar build pipeline. Any change to how FounderOrchestrationService behaved risked breaking the language extension, and vice versa — two things that have no reason to know about each other were suddenly entangled.

I wrote ADR-009 to reverse that decision and draw a hard boundary: this extension is scoped strictly to Langium concerns — grammar, generated parser/AST, language services, the language client and server, and grammar-scoped tests. No application services, no repository instantiation, no domain event wiring, no shared infrastructure code. If LeanAgileScript needs to talk to the rest of the platform someday, that has to happen through a real integration boundary, not through an import sitting next to a language client.

That boundary is holding — I checked the current source while writing this, and there's no trace of FounderOrchestrationService, PouchDB, or the event bus anywhere in the three packages. But staying honest about scope means I have to name the parts that are still unfinished, too:

  • The CLI's generate command is a stub. It parses and validates a .lascript file correctly, but the actual code-generation step writes a placeholder file with a // TODO: place here generated code comment. Parsing works; generating real output from a parsed model does not, yet.
  • There's no standalone validate command — validation currently happens only as a side effect of running generate, which isn't the same as a first-class "check this file" workflow.
  • The grammar has scaffolding for a host-system type layer alongside the type-system layer that's actually built, but those directories are empty placeholders. The type system — data types, object types, collections, constraints — is the only part that's real today.
  • All three packages are still versioned 0.0.1. That's not a knock on the work; it's an accurate label for where it is in its lifecycle.

None of that makes the extension not worth building. It means the extension is a language-tooling foundation with one working layer, not a finished acceptance-testing pipeline — and I'd rather tell you that directly than let the grammar snippet above imply more than the code currently does.

Where This Is Going

The reason I built the type system first, before code generation, is that the type system is the part that has to be right before anything downstream — generated fixtures, generated test scaffolding, generated documentation — can be trusted. A DSL that lets you define a Decimal constrained to Min -90, Max 90 and then hands that definition to a generator is a DSL that can catch a mismatched constraint before a human ever writes the corresponding validation code by hand and gets it slightly wrong.

That's the acceptance-test-driven workflow I write about at length in The Lean-Agile MVP Revolution, the founder-facing book I'll cover later in this series — the idea that a specification should be the source of truth a test suite verifies against, not a document a test suite quietly drifts away from. LeanAgileScript is the tooling half of that idea. The book is the process half. Neither one is finished, and I think that's the right order to be honest about both of them in public.

Next up, though, is a different kind of specification problem: the reference I wrote after one too many debugging sessions where the failure was in the prompt, not the code.

If you've built or used a structured-specification language before — Gherkin, TLA+, something in-house — I'd be curious what held up under real use and what didn't. That's exactly the kind of lesson I'd rather learn from someone else's five years than my own next five.

← All articles