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:
- Information hiding. Each module is organized around a design decision it encapsulates. Callers depend on the interface; the encapsulated decision can change without disturbing them.
- Deep abstractions. A module's value is proportional to how much implementation it hides behind how small an interface.
- Decomposition by change axis. Modules are drawn around what is likely to change — not around data structure or algorithm steps.
- Stable contracts. A module's interface is more stable than its implementation, and more stable than the requirements that motivated it.
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:
- Design-time: a target for human review and modularity auditing before code exists.
- 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
- System purpose (one paragraph).
- Capability registry: every capability declared exactly once, with its full contract. Module sections in
modules.mdreference capabilities by Markdown link; they do not redefine them. - Provider map: which module provides which capability. One provider per capability. Derivable from each capability's
Providerlink, but maintained explicitly inindex.mdfor navigability. - Consumer map: which modules consume which capabilities. Derivable by walking the Depends on links in
modules.md— typically generated, not hand-maintained. - Modularity audit: results of the checks in §7.
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
- Encapsulation is named in caller-irrelevant terms. "How sessions are stored, expired, and revoked" — good. "Uses Redis with TTL" — leaks.
- Capability names don't leak implementation.
flush_to_disk()leaks;commit()doesn't.cache_evict()leaks;forget(handle)doesn't. - Dependencies are on capabilities, not modules. Say opaque-bytes-storage, not PostgresStore.
- No types, signatures, or language idioms.
Promise<Result<Session, Error>>is implementation. - 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
- Verb-phrased. "issue", "resolve", "find-by-prefix" — verbs. Nouns like
session()are smells; usually a leaked data structure. - Inputs and outputs by role, not type. "subject" and "handle", not
stringandUUID. - Failure modes are caller-meaningful. "not-authorized" tells a caller something. "RedisTimeoutException" leaks.
- 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:
- Bounded contexts — module clustering
- Actors and actions — capabilities
- Anticipated change axes — encapsulations
- External boundaries — edge capabilities
- Cross-cutting concerns — shared capabilities
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:
- EARS suits event-driven, edge-case-heavy, or safety-critical work.
- User stories with acceptance criteria suit user-facing and product-led work.
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.
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.
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.
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.
Read Actors and External boundaries to identify capabilities. Each actor's action is a candidate capability; each boundary suggests capabilities at the edges.
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.
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.
Add cross-cutting capabilities. Cross-cutting concerns become shared capabilities — clock, identity, logging, configuration. High fan-in is expected.
Run the modularity audit (§7). Iterate until it passes.
6.1 Across paradigms
LAMP is uniform across paradigms; only what gets encapsulated differs:
- Functional code: data representation and algorithm choice.
- Object-oriented code: state representation and lifecycle.
- UI code: layout and presentation, behind a behavioral contract (observable state, available actions, feedback). Visual specifics are not in LAMP.
- API code: transport, serialization, and auth. Capabilities are domain operations stripped of HTTP/RPC vocabulary.
- Jobs and batch: scheduling, partitioning, and retry. Capability contracts annotate idempotence and ordering as part of Effects.
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?
- 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.
- 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.
- 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.
- 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?
- All capability links resolve. Every link in a module's Capabilities or Depends on section points to an existing
### capability: <name>heading inindex.md. Broken links are the linked-spec equivalent of an undefined symbol; code generation cannot proceed past them. - 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-providercapabilities (e.g., type-class-style dispatch) it means one binding per scope, and the audit verifies each scope rather than the global graph. - 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.mdis derivable from this walk. - 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.
- 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.
- 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
- Shallow modules: many capabilities, thin encapsulation. Often a wrapper around a data structure.
- Implementation as encapsulation: "Encapsulates: uses PostgreSQL with row-level locking" is implementation. The encapsulated decision is how concurrent edits are reconciled.
- God capability: many inputs and outputs hiding several capabilities.
- Verb-less capabilities:
session(),user(),config()— usually a leaked data type. - Hypothetical flexibility: change axes nobody is asking for. Each must trace to design.
- Spec-task drift: downstream tasks reference work LAMP doesn't describe.
- Reused capability for different meanings: same name, different contracts. Rename one.
10. Authoring workflow
- Begin from a
lamp/intent.mdthat meets §5.1. - Draft modules and encapsulations first, drawing from Bounded contexts and Anticipated change axes. Don't write capabilities until each module has a justified encapsulation.
- Add capabilities and route them through the registry.
- Write capability contracts in the registry — guarantees, failure modes, and any cross-capability behavior.
- Run the modularity audit. Iterate until it passes.
- Hand LAMP to the agent or implementer as the binding contract. The agent generates implementation and tests.
- When requirements change, update
lamp/intent.mdfirst, then LAMP, then regenerate.
11. What LAMP deliberately does not do
- Specify types, error class hierarchies, async-ness, or language idioms.
- Describe layout, styling, or visual design.
- Describe deployment, infrastructure, or operations.
- Describe individual functions inside a module.
- Capture sequence diagrams or temporal flows beyond what fits in capability contracts (requires, guarantees, operational notes).
- Include a separate validation artifact. Tests are a code-generation output, derived from the same capability contracts the implementation honors.
- Replace requirements or design.
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."