LAMP — Language-Agnostic Module rePresentation

May 18, 2026

A proposal for an intermediate representation that captures software modularity before code is generated.


1. Overview

1.1 Motivation

Agentic code generation produces code that compiles, passes tests, and looks reasonable — but often lacks coherent modular structure. Modules end up sized to fit a generation step rather than to encapsulate a decision. Boundaries drift toward whatever the agent finds convenient. Implementation details surface in interfaces because the agent has no reason to hide them. The code works; it doesn't earn the modularity it implicitly claims.

The artifacts that feed generation today don't address this. Requirements describe behavior, not decomposition. Design describes approach, not module contracts. Tests verify behavior, not whether it's housed in the right boundaries. Code review catches modularity problems only after they're encoded. Prompts are invocations, not contracts — what one prompt asks for, the next forgets. LAMP fills the gap with a reviewable artifact that commits to modular intent before code exists: each module names what it encapsulates and what it exposes, capability contracts the agent must honor, and a modularity audit that gates generation rather than reviewing it afterward.

1.2 What LAMP is

LAMP sits between architectural design and code. It captures a system's modular structure in a form that is reviewable before any implementation exists — so the agent or human generating code is constrained on what matters (module boundaries, encapsulation, observable contracts) and free on what doesn't (types, languages, data structures, concurrency).

LAMP is one stage in a pipeline:

user conversations
   → refine into structured requirements
user stories or EARS
   → extract architectural intent
intent.md
   → decompose into modules around encapsulated decisions
LAMP
   → generate implementation and tests honoring the contracts
code + tests

LAMP rests on four principles drawn from established modularity practice:

LAMP is not UML, an IDL, OpenAPI, or a class diagram. Those describe surfaces (signatures, schemas, relationships); LAMP describes responsibilities — what each module is responsible for hiding.

LAMP is paradigm-agnostic in both substance and vocabulary. The capability contract terms — Requires, Guarantees, Purity — apply equally to pure functions, stateful objects, message handlers, and effectful procedures. Examples in this document draw from multiple paradigms.

LAMP serves two jobs:

  1. Design-time: a target for human review and modularity auditing before code exists.
  2. Generation-time: a contract the agent honors when producing code and tests. Tests derive from the capability contracts stated in LAMP and serve as their behavioral verification.

1.3 Status

This document is a working proposal, not a stabilized standard. It has not been validated on a system at scale; refinements are expected as it meets real practice. The aim of publishing it is to invite the kind of criticism that improves the format — not to claim a finished position.


2. Document layout

A LAMP artifact is a directory of Markdown files. Markdown is human-readable, diff-friendly, and parseable — and resists the YAML-soup trap where structure displaces thought.

lamp/
  intent.md            # Architectural intent — LAMP's input contract
  index.md             # System summary, capability registry, audit
  modules.md           # All module manifests in one file

2.1 intent.md

Architectural intent feeding LAMP — a focused extract of the team's broader design. Six sections, fully specified in §5.

2.2 index.md

2.3 modules.md

All module manifests in one file. Each module is a top-level section. Structure given in §3.

A single file is the default because LAMP modules are small (typically under twenty lines each), the agent benefits from seeing the whole modular structure when generating any one module, and reviewers assess modularity holistically. For systems with many modules (roughly 30+) where the file becomes unwieldy, modules may be split into a modules/<module-name>.md directory with one file per module — the format of each module is unchanged.


3. The module specification

A module is intentionally a thin manifest. Its job is to give the system a clean unit of code organization with a clear encapsulation and a declared surface — nothing more. Behavior lives in the capability registry; change-axis rationale lives in design. The module manifest is the place where a reviewer (or an agent) sees the modular shape of the system at a glance.

Each module is a top-level section in modules.md, with these subsections in order. All are required.

# Module: <name>

## Encapsulates
The design decision or complexity this module hides. State it in
caller-irrelevant terms. If it doesn't fit in one or two sentences,
the module is not justified — split, merge, or rethink.

## Capabilities
Markdown links into the capability registry in `index.md`, one per
line. An optional inline summary may follow each link as a hint —
the registry remains the source of truth.

  - [issue-session](index.md#capability-issue-session) — produces a session handle
  - [resolve-session](index.md#capability-resolve-session)
  - [revoke-session](index.md#capability-revoke-session)

## Depends on (capabilities)
Markdown links into the capability registry, one per line.
Dependencies are on capabilities, never on other modules directly.

  - [monotonic-clock](index.md#capability-monotonic-clock)
  - [opaque-bytes-storage](index.md#capability-opaque-bytes-storage)

The module manifest answers three questions and no others: what does this module hide, what does it offer, what does it need?

If the per-file scaling option is used (modules split into modules/<module-name>.md files), capability links use ../index.md#capability-<name> — one extra level up to escape the directory.

3.1 Authoring rules

  1. Encapsulation is named in caller-irrelevant terms. "How sessions are stored, expired, and revoked" — good. "Uses Redis with TTL" — leaks.
  2. Capability names don't leak implementation. flush_to_disk() leaks; commit() doesn't. cache_evict() leaks; forget(handle) doesn't.
  3. Dependencies are on capabilities, not modules. Say opaque-bytes-storage, not PostgresStore.
  4. No types, signatures, or language idioms. Promise<Result<Session, Error>> is implementation.
  5. Sized by encapsulation, not lines of code. A module hiding "we use a hash map" is shallow — absorb it.

4. The capability specification

Capabilities live in index.md's registry. Each is declared once. The heading format is fixed — module manifests link to capabilities by anchor, so the heading is part of the contract.

### capability: <verb-phrase-name>

Description: one sentence on what this accomplishes for a caller.

Inputs: by role, not by type.
  - subject: the principal the operation acts on behalf of
  - claims: caller-supplied attributes

Outputs: by role, not by type.
  - handle: an opaque reference identifying the session

Requires: what must hold for the call to be valid.
  (For pure capabilities: constraints on arguments.
   For stateful capabilities: constraints on world state plus arguments.)

Guarantees: what is true on successful completion.
  (For pure capabilities: properties of the return value.
   For stateful capabilities: properties of the new state plus return value.)

Failure modes: caller-meaningful categories (not exception classes).
  - invalid-input
  - capacity-exhausted
  - not-authorized

Purity: pure | impure
Operational notes (when impure): idempotent? blocking? ordering?

Provider: [<module-name>](modules.md#module-<module-name>)

The Provider field is a link to the module's section in modules.md — the capability registry and module manifests form a navigable graph in both directions. (If the per-file scaling option is used, links take the form modules/<module-name>.md.)

4.1 Authoring rules for capabilities

  1. Verb-phrased. "issue", "resolve", "find-by-prefix" — verbs. Nouns like session() are smells; usually a leaked data structure.
  2. Inputs and outputs by role, not type. "subject" and "handle", not string and UUID.
  3. Failure modes are caller-meaningful. "not-authorized" tells a caller something. "RedisTimeoutException" leaks.
  4. One provider per binding scope. A capability has one provider in the scope where consumers reference it. For most capabilities this means one provider globally, and two providers is a factoring error. Some paradigms support multi-instance capabilities (Haskell type classes, Rust traits, OO interfaces with many implementations) — these capabilities are marked multi-provider, and each consuming scope binds to one specific provider. The audit's single-provider check applies per binding scope, not globally.

5. The intent file

LAMP is generated from lamp/intent.md — a focused extract of the team's broader design containing the architectural information LAMP needs and nothing else. Concerns the broader design might cover (operational topology, data schemas, deployment plans, NFRs, security architecture, UI mockups) are out of scope here.

The intent file is typically derived from existing design artifacts — ADRs, system design documents, whiteboard captures — or written from scratch using requirements as raw material. Either way, the contents must conform to §5.1. Without it, modules reflect accidents of writing rather than considered architectural intent.

5.1 Intent file specification

lamp/intent.md has six sections, in this order. Each is bounded — what belongs, and what doesn't.

1. Problem framing. Contains: what the system solves, who it serves, and the constraints shaping the approach. One or two paragraphs is usually enough. Excludes: solution-shaped statements. This section is the why — the context a reviewer needs to evaluate whether the rest of the extract is sensible.

2. Bounded contexts and domain decomposition. Contains: major domains and sub-domains, the language used in each, and the boundaries between them. Bounded contexts are the primary source of module clusters because they're already organized around what varies independently. Excludes: technical decomposition into services, layers, or tiers. Those are deployment choices, not domain choices.

3. Actors and their actions. Contains: the actors (human roles, other systems, scheduled processes) who interact with the system, and what each does. Each action is a candidate capability. Group by actor and domain. Excludes: UI flows, screen-by-screen interactions, wire-protocol details. The action is "submit claim"; the form and HTTP verb are downstream.

4. Anticipated change axes. Contains: what may change over the system's lifetime — backends, policies, algorithms, integrations, scale points, regulations. Each axis is the seed of an encapsulation: a module hides the decision a change axis anticipates. Excludes: hypothetical flexibility not grounded in a current concern or credible future scenario.

5. External boundaries. Contains: systems, services, and protocols this system meets at its edges — third-party APIs, partner integrations, regulatory interfaces, neighboring internal services. Each named with the contract it imposes. Excludes: the internals of those external systems. Treat them as black boxes.

6. Cross-cutting concerns. Contains: concerns that traverse modules — identity, time, logging, configuration, audit, feature flags, observability. Named here so they appear as shared capabilities in LAMP's registry rather than being reinvented per module. Excludes: implementation strategies (e.g., "use OpenTelemetry"). The implementation is encapsulated by the owning module.

5.2 What the intent file does and does not feed

Five sections directly feed LAMP content:

Problem framing does not generate LAMP content but is necessary for reviewers to evaluate whether the modules and capabilities make sense for this system.

5.3 Folder structure

project/
  requirements/         # EARS or user stories, distilled from conversations
    ...
  lamp/
    intent.md           # Architectural intent — see §5.1
    index.md            # Capability registry, provider map, audit
    modules.md          # All module manifests
  src/                  # Implementation generated from LAMP
  tests/                # Tests generated from LAMP capability contracts

lamp/intent.md lives inside lamp/ because it is LAMP's input contract, versioned alongside the modules it feeds. The team's broader design — wherever it lives in the project — is the source from which the intent file is derived.

5.4 Requirements as a source

When written from scratch, the intent file draws on requirements. The notation is a team and domain choice:

LAMP requires only that the requirements be disciplined enough to support architectural reasoning. Turning them into the six-section intent file is a deliberate transformation, not a mechanical one.


6. Generating LAMP from lamp/intent.md

The procedure is deterministic enough for a human or agent to follow.

  1. Read Bounded contexts and Anticipated change axes first. Bounded contexts give candidate clusters; change axes give candidate encapsulations. Together they tell you where module boundaries fall.

  2. Cluster change axes by what would change together, within a bounded context. Two change axes belong to the same module if a plausible future change would touch both; to different modules if they vary independently. Clusters rarely cross context boundaries.

  3. For each cluster, name what it encapsulates. One or two sentences in caller-irrelevant terms, drawing from the change axes themselves. If the encapsulation can't be stated cleanly, re-cluster.

  4. Read Actors and External boundaries to identify capabilities. Each actor's action is a candidate capability; each boundary suggests capabilities at the edges.

  5. Assign each capability to the module whose encapsulation it belongs to. A capability that doesn't fit cleanly signals wrong modules or poor capability factoring.

  6. Write capability contracts in the registry. Guarantees, failure modes, and any cross-capability behavior (e.g., "after revoke-X, resolve-X returns invalid") live in the capability spec — not in module manifests. Guarantees come from properties the intent file commits to; failure modes come from edge cases and unwanted-behavior responses.

  7. Add cross-cutting capabilities. Cross-cutting concerns become shared capabilities — clock, identity, logging, configuration. High fan-in is expected.

  8. Run the modularity audit (§7). Iterate until it passes.

6.1 Across paradigms

LAMP is uniform across paradigms; only what gets encapsulated differs:


7. The modularity audit

The audit is the quality gate before code generation. It verifies that each module encapsulates the right thing, that the system exposes the right capabilities, and that the spec can be handed to a coding agent with confidence — clean boundaries, no premature commitments, coherent architecture. A spec that fails the audit has problems an agent will faithfully translate into bad code.

Per-module checks ask whether each module is justified and well-formed. System-wide checks ask whether the modules fit together coherently.

7.1 Per-module checks

These ask: is this module's encapsulation real, deep, leak-free, and justified by design?

  1. Encapsulation is statable. One or two sentences, free of implementation vocabulary (no library names, protocols, data structures). A vague encapsulation produces code without real boundaries.
  2. No leaky names. No capability name references implementation. A project-specific banned-words list ("cache", "flush", "redis", "json", "retry") catches most violations. Leaky names produce code that's hard to swap or refactor.
  3. Interface-to-encapsulation ratio. Many capabilities over a thin encapsulation is shallow. Flag any module whose encapsulation fits in one short sentence yet exposes more than a few capabilities. Shallow modules produce code that doesn't earn its existence — ceremony around a thin abstraction.
  4. Encapsulation traces to intent. Each module's encapsulation maps to one or more entries in lamp/intent.md's Anticipated change axes. Modules with no provenance in the intent file produce code organized around speculation rather than real architectural intent.

7.2 System-wide checks

These ask: do the modules fit together, with no gaps, no overlaps, and no tangles?

  1. All capability links resolve. Every link in a module's Capabilities or Depends on section points to an existing ### capability: <name> heading in index.md. Broken links are the linked-spec equivalent of an undefined symbol; code generation cannot proceed past them.
  2. Single provider per binding scope. A capability has one provider in each scope where it's consumed. For ordinary capabilities this means one provider globally; for multi-provider capabilities (e.g., type-class-style dispatch) it means one binding per scope, and the audit verifies each scope rather than the global graph.
  3. No orphan capabilities. Provided-but-unconsumed is dead weight; consumed-but-unprovided is a gap. Computed by walking module manifests: a capability is consumed if any module's Depends on links to it; provided if any module's Capabilities links to it. The consumer map in index.md is derivable from this walk.
  4. Acyclic capability graph (default). Cycles deserve scrutiny — sometimes legitimate, often a sign of shared encapsulation that should merge or split differently. Cycles in the spec produce circular dependencies in code.
  5. Healthy fan-in distribution. A few high-fan-in capabilities (logging, clock, identity) is normal. Uniform high fan-in suggests insufficient layering and produces tangled code.
  6. Vocabulary is consistent. The same domain concept is named the same way across modules. Inconsistent vocabulary produces code that's hard to navigate.

7.3 Audit output

Appended to index.md:

## Modularity audit — <date>

Per-module:
  - SessionStore: PASS
  - RateLimiter: WARN — encapsulation references "Redis"
  - AuditLog: FAIL — two providers for capability `record-event`

System-wide:
  - Orphan capabilities: none
  - Cycles: SessionStore <-> AuditLog (review)
  - Fan-in: capability `now()` consumed by 14 modules (expected)

A FAIL means the spec, if handed to an agent today, will produce code with a known modularity defect — not ready for code generation. WARNs are not blocking but should be reviewed; they often indicate drift toward leakage that the next iteration will struggle to reverse.


8. A worked example

From a user story:

As a service, I want to issue, validate, and revoke session handles so I can recognize returning callers without rechecking credentials on every call.

Acceptance criteria:

  • Given a valid subject, when I issue a handle, resolving it returns the original subject.
  • Given a revoked handle, resolving it returns invalid.
  • Given an expired handle, resolving it returns invalid.
  • Two handles for the same subject are distinct and unguessable.

lamp/intent.md captures the relevant content: the system manages session handles for callers (problem framing); Sessions is a bounded context; the actor is callers with actions issue, validate, revoke; anticipated change axes include storage backend, token format, revocation strategy; external boundaries name the calling services; cross-cutting concerns include time (for expiration).

LAMP excerpts:

index.md capability registry:

### capability: issue-session

Description: produces an opaque handle representing an authenticated subject.
Inputs:
  - subject: the principal the session represents
  - claims: caller-supplied attributes
Outputs:
  - handle: opaque reference
Guarantees:
  - The handle is valid until revoked or expired.
  - The handle is distinct from every previously-issued handle.
  - The handle is not guessable from prior handles.
Failure modes:
  - capacity-exhausted
Purity: impure
Provider: [SessionStore](modules.md#module-sessionstore)

### capability: revoke-session

Description: invalidates an issued handle.
Inputs:
  - handle: an opaque reference previously returned by issue-session
Guarantees:
  - After this returns, resolve-session(handle) returns invalid.
  - Subsequent revoke-session(handle) calls are accepted with no further effect.
Purity: impure
Operational notes: idempotent.
Provider: [SessionStore](modules.md#module-sessionstore)

modules.md (SessionStore section):

# Module: SessionStore

## Encapsulates
How sessions are stored, expired, and revoked. Whether storage is
in-memory, in a database, or encoded into the handle itself is hidden.
Whether revocation is push-based or pull-based is hidden.

## Capabilities
- [issue-session](index.md#capability-issue-session) — produces a session handle
- [resolve-session](index.md#capability-resolve-session) — looks up subject and claims
- [revoke-session](index.md#capability-revoke-session) — invalidates a handle

## Depends on
- [monotonic-clock](index.md#capability-monotonic-clock)
- [opaque-bytes-storage](index.md#capability-opaque-bytes-storage)

Caller-observable behavior — guarantees, the relationship between revoke and resolve, idempotence of revoke — lives in the capability registry. Change rationale lives in the intent file's Anticipated change axes. The agent is constrained by names and contracts, free to choose Redis, in-memory, or signed tokens. If storage moves later, only the implementation moves; LAMP is unchanged.

8.1 A pure-function example

To show that the same format applies outside stateful, OO-flavored systems, consider an ExpressionParser module — a pure parser for arithmetic expressions.

### capability: parse-expression

Description: parses a textual arithmetic expression into a structured form.
Inputs:
  - source: the expression text to parse
Outputs:
  - tree: the parsed expression structure | parse-error with location
Requires:
  - source is a finite string (no constraints on validity — invalid input
    produces a parse-error, not a failure)
Guarantees:
  - For any input, the output is either a valid tree or a parse-error
    naming the position of the first unparseable token.
  - parse-expression(s) is deterministic — calling twice with the same
    source returns the same result.
Failure modes:
  - (none — invalid inputs return parse-error in the output, not via
    failure modes; failure modes are reserved for operational failures
    that can occur even on valid inputs)
Purity: pure
Provider: [ExpressionParser](modules.md#module-expressionparser)
# Module: ExpressionParser

## Encapsulates
The parsing strategy and grammar representation. Whether the parser is
recursive-descent, table-driven, or combinator-based is hidden. The
specific grammar variant (precedence rules, supported operators) is
hidden behind a documented expression syntax.

## Capabilities
- [parse-expression](index.md#capability-parse-expression) — parses arithmetic text into a tree

## Depends on
(none)

Note what the format keeps and what it changes. Requires and Guarantees read naturally for a pure function — there's no implied state mutation. Purity: pure lets readers and tooling skip the operational-notes section entirely. The module has no dependencies because pure parsing needs nothing beyond its input. The encapsulation is a real design decision (parsing strategy, grammar variant) — exactly what LAMP exists to surface.


9. Anti-patterns and smells


10. Authoring workflow

  1. Begin from a lamp/intent.md that meets §5.1.
  2. Draft modules and encapsulations first, drawing from Bounded contexts and Anticipated change axes. Don't write capabilities until each module has a justified encapsulation.
  3. Add capabilities and route them through the registry.
  4. Write capability contracts in the registry — guarantees, failure modes, and any cross-capability behavior.
  5. Run the modularity audit. Iterate until it passes.
  6. Hand LAMP to the agent or implementer as the binding contract. The agent generates implementation and tests.
  7. When requirements change, update lamp/intent.md first, then LAMP, then regenerate.

11. What LAMP deliberately does not do

Each is a real concern handled by an adjacent stage. Conflating them with LAMP would dilute its value as a modularity contract.


12. Versioning

LAMP lives in source control alongside the code it describes. A change to what a module encapsulates or to a capability's contract is a spec change, reviewed like a breaking API change. Internal implementation changes don't touch LAMP. If everything triggers a LAMP change, the encapsulations aren't really hidden.


Appendix A. Example prompt for generating lamp/intent.md

A starting point — not canonical. Teams should adapt it; the contract that matters is the §5.1 specification.

Generate `lamp/intent.md` — the LAMP intent file. It must contain
exactly the following six sections, in order, and nothing outside
their scope:

1. Problem framing — what the system solves, who it serves, the
   constraints shaping the approach. One or two paragraphs.

2. Bounded contexts and domain decomposition — major domains and
   sub-domains, the language used in each, the boundaries between
   them. Do not decompose into services, layers, or tiers.

3. Actors and their actions — who interacts with the system (human
   roles, other systems, scheduled processes) and what each does.
   Group by actor and domain. No UI flows or wire-protocol details.

4. Anticipated change axes — what may change over the system's
   lifetime: backends, policies, algorithms, integrations, scale
   points, regulations. Exclude hypothetical flexibility not
   grounded in a current concern or credible future scenario.

5. External boundaries — systems, services, and protocols at the
   edges, each named with the contract it imposes. Treat external
   systems as black boxes.

6. Cross-cutting concerns — concerns that traverse modules (identity,
   time, logging, configuration, audit, observability). Name the
   concerns; don't specify implementation strategies.

Exclude from all sections: data schemas or DDLs, sequence diagrams,
fine-grained call graphs, infrastructure, network topology,
deployment plans, NFRs, UI mockups, API wire formats, type
signatures, module names, capability names. Those belong to
parallel artifacts (operational design, code generation, LAMP
itself).

Sources: requirements written as EARS or user stories, existing
architecture documents, ADRs, system design notes, or any
combination thereof. The output is a single Markdown file at
`lamp/intent.md`.

The exclusions are load-bearing — most agent failures come from helpfully adding implementation detail because it feels like "real design."

← All posts