MM3journeys

Alignment · 2026-10-16

Type one sentence. Get the architecture right — in any language.

Watch a real run: an empty project, one sentence, a recipe, working code, and a check that says yes to good code and no to a broken copy.

  • 1sentence from you
  • 6languages, same questions
  • 1check: good code passed, broken copy failed
  • $0.00005per check, about (estimated from tokens)

Scroll. The panel follows the story.

1 · You

You type one sentence.

Open your coding agent in a project that has almost nothing in it. Type: add hexagonal architecture to this project.

That is the whole request. No folder layout, no pasted guide. Without a recipe, an agent invents its own “hexagonal” from memory, and nothing says it got it right.

Hexagonal architecture is a way to lay out code so the business rules never depend on the plumbing around them (the database, the web layer, the screen). Its inventor, Alistair Cockburn, also calls it “ports and adapters”.

ADAPTERS · inboundwho calls youADAPTERS · outboundwhat you callPORTSCOREPORTSBusiness rulesreject qty of 0 or lessweb adapterCLI adaptertests adapterentry point (main)memory store /databaseadapterorder inputportstore portcallsbuilds & passes inimplementsimports directlyPort = a socket the core owns. Adapter = a plug that fits it.callsimplementsbuilds & passes inbreaks the ruleADAPTERS · inboundwho calls youPORTSCOREBusiness rulesreject qty of 0 or lessPORTSweb adapterCLI adaptertests adapterorder inputportstore portmemory store /database adapterentry point(main)imports directlyADAPTERS · outboundwhat you callPort = a socket the core owns.Adapter = a plug that fits it.callsimplementsbuilds & passes inbreaks the rule
The standard picture: callers come in through the input port, storage plugs into the store port, and the business rules in the middle meet both only through ports.The broken line: the business rules reach straight into the storage code and skip the port. This is what MM3 catches.
> add hexagonal architecture to this project
$ ls
Cargo.toml
$ cat Cargo.toml
[package]
name = "app"
version = "0.1.0"
edition = "2021"
One file. A brand-new Rust app.

2 · Your agent

Your agent asks MM3 how.

You do nothing. Your agent sends your sentence to MM3 and a recipe comes back. It even works out the language on its own, from Cargo.toml.

The one rule that matters: the business logic never reaches into the database code directly. The business rule talks only to its own socket.

The recipe is not made up. It traces to Alistair Cockburn’s own page, dated 2005-09-04. MM3 checked that the name, the date and the intent are quoted from that page. The four parts and the rules are our reading of his intent, and the recipe says so itself.

$ node mcp/call.mjs apply-pattern '{"request":"add hexagonal architecture to this project"}'
1. Pattern: arch.pattern.hexagonal (Hexagonal Architecture (Ports and Adapters)), resolved from the request (alias match: "hexagonal architecture").
2. Language: detect it from the project's OWN files
   - Cargo.toml -> rust
[...]
RULES R1: The domain core does not import or reference any adapter.
SOURCES https://alistair.cockburn.us/hexagonal-architecture/ (Alistair Cockburn, as of 2005-09-04, tier A)

In plain words, the recipe has four parts:

  • The business ruleWhat your app is actually for: reject an order of zero or fewer, otherwise save it.
  • The portA socket the rule declares: “save an order”.
  • The adapterOne plug that fits the socket, outside the rule: a store that keeps orders in memory.
  • The wiringThe start-up file that picks a plug and hands it to the rule.
The rule The business rule never imports an adapter. (R1, Cockburn, 2005)

3 · Your agent

It writes the code, in your language.

Still nothing for you to do. The agent writes the files the recipe names, builds the project and runs it. It prints [("tea", 2)]: an order of two teas, saved.

The honest wrinkle. The first build failed. Cargo looks for src/main.rs, and the recipe puts main.rs at the project root. The recipe did not say so. The agent added a three-line block to your Cargo.toml and told MM3 about the gap, so the recipe can be fixed for the next person.

files the agent wrote
  • main.rs the wiring
  • domain/order.rs the business rule
  • domain/ports.rs the socket (port)
  • domain/mod.rs
  • adapters/memory_store.rs the plug (adapter)
  • adapters/mod.rs
  • bind.json a note: which file plays which part
  • Cargo.toml yours, plus one addition (below)
$ cargo build
error: failed to parse manifest at `.../Cargo.toml`
Caused by:
  no targets specified in the manifest
+ [[bin]]
+ name = "app"
+ path = "main.rs"
$ cargo build
   Compiling app v0.1.0 (...)
   Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.65s
$ ./target/debug/app
[("tea", 2)]

4 · MM3

MM3 checks it.

You say yes to one paid check (we said yes, once). The result, run MM3-0371: pass.

The check is a fixed list of 12 questions: 9 yes/no, one overall goal, two judgement calls. The panel shows three of the nine. A score is how sure the checker is that the answer is “yes”, and for these, yes is the healthy answer.

The questions are locked with a fingerprint, so nobody can reword them later to make a result come out better. The check cost about $0.00005 (estimated from tokens).

PASSrun MM3-0371 · overall goal 0.96

The business rule keeps to itself

0.98

Q1. Does the order rule in the domain folder itself reject a non-positive quantity?

0.97

Q2. Is the order rule in the domain folder free of any import of an adapter module? (Does the business logic avoid importing the database-side code?)

0.88

Q3. Does the order rule in the domain folder import only from the domain or the standard library?

Q4 to Q9 ask about the socket, the plug and the start-up file. All six pass too; the lowest score is 0.84.

5 · Try it

Is it just saying yes? Break one line.

A check that always says yes is worthless. So we made a copy that breaks exactly one rule: the business rule reaches into the database-side code, which is exactly what the one rule forbids. We then asked the same questions again. Flip the switch to do it yourself.

Watch the marked check fall (Q2 and Q3 both do, while Q1 stays high: the rule still rejects a zero quantity). A check that is not about the broken rule mostly holds, so the check is not just shouting “fail” at everything. It starts in Rust; every score comes with its real run.

ADAPTERS · inboundwho calls youADAPTERS · outboundwhat you callPORTSCOREPORTSBusiness rulesreject qty of 0 or lessweb adapterCLI adaptertests adapterentry point (main)memory store /databaseadapterorder inputportstore portcallsbuilds & passes inimplementsimports directlyPort = a socket the core owns. Adapter = a plug that fits it.callsimplementsbuilds & passes inbreaks the ruleADAPTERS · inboundwho calls youPORTSCOREBusiness rulesreject qty of 0 or lessPORTSweb adapterCLI adaptertests adapterorder inputportstore portmemory store /database adapterentry point(main)imports directlyADAPTERS · outboundwhat you callPort = a socket the core owns.Adapter = a plug that fits it.callsimplementsbuilds & passes inbreaks the rule
The good copy: the business rules know only their ports.The broken copy: the business rules reach straight into the storage code. This is what MM3 catches.
off: the good copy (Hexagonal, Rust)

Without JavaScript, here is the Rust pair in words, then all six pairs. In Rust the broken copy adds use crate::adapters::memory_store::MemoryStore; to domain/order.rs (and a function that builds one). Result: MM3-0380, fail, goal 0.16; Q2 0.07, Q3 0.07, Q1 still 0.93.

LanguageGood copyBroken copy
TypeScriptMM3-0101 passMM3-0373 fail
GoMM3-0102 passMM3-0374 fail
PythonMM3-0103 passMM3-0375 fail
JavaMM3-0104 passMM3-0376 fail
RustMM3-0371 passMM3-0380 fail
C#MM3-0106 passMM3-0378 fail
Hexagonal · domain/order.rsgood copy
PASSgood code accepted — MM3-0371 · goal 0.96Q2 0.97
// Domain core: the order rule. It knows only the port, never an adapter.use super::ports::OrderStore;pub fn place_order(store: &mut impl OrderStore, item: &str, qty: i64) -> Result<(), String> {    if qty <= 0 {        return Err("quantity must be positive".to_string());    }    store.save(item, qty as u32);    Ok(())}
0.98

Q1. Still rejects a zero-or-less quantity?

0.97

Q2. Free of any import of an adapter?

0.88

Q3. Imports only from the domain or the standard library?

A score is how sure the checker is that the code is healthy on that line. The marked check is the one about the rule that gets broken.

6 · Try it

Pick your language.

The same questions work in any language, because they ask about roles (the business rule, the socket, the plug, the start-up file), not one language’s syntax. The recipe finds your language from your project’s own files.

Pick one. The panel swaps in that app’s real business-rule file and the real result for it. The switch from the last step carries over, so you can see the broken copy in any language too.

7 · The result

What it comes to.

One sentence from you. A recipe from the person who named the pattern. Working code. And a verdict you can read: yes to the good copy, no to the broken one, in all six languages, naming the part that broke.

1 sentence from you
6 languages, the same nine yes/no questions
1 check: good copy passed, broken copy failed, every time
$0.00005 a check, about (estimated from tokens)
------
MM3-0371 pass  →  MM3-0380 fail (Rust, the live pair)
all six languages pass  →  fail (MM3-0101 to 0104, 0106, 0371 vs MM3-0373 to 0376, 0378, 0380)

8 · A real app

A real integration: swap anything, test everything

Real apps lean on outside services: a payment API, a database, file storage, email. Here each one gets a socket that the business rule owns. Plug the real service in for production, plug an in-memory fake in for tests, and the rule never knows the difference. That is what lets you write the tests first.

The app: placing an order checks the quantity is positive, charges the card, saves the order, stores a PDF receipt and emails it. One agent got one sentence (“add hexagonal architecture to this order service, build it test-first”) and wrote everything below. No network calls: the live plugs build the request they would send.

ADAPTERS · inboundwho calls youADAPTERS · outboundwhat you callPORTSCOREPORTSOrder serviceplaceOrder, thebusiness rulenow running:productiontestsweb, API, testsadapterentry point (main)payment APIadapterlivein-memoryadapterfake (tests)Postgres-styleDB adapterlivein-memoryadapterfake (tests)S3-style objectstorage adapterlivein-memoryadapterfake (tests)email APIadapterlivein-memoryadapterfake (tests)place-orderportpayment portdatabase portreceiptsstorage portemail portcallsbuilds & passes inimplementsPort = a socket the core owns. Adapter = a plug that fits it.callsimplementsbuilds & passes inADAPTERS · inboundwho calls youPORTSCOREOrder serviceplaceOrder, the business rulenow running: productiontestsPORTSweb, API, testsadapterplace-orderportpaymentportpaymentAPIadapterlivein-memoryadapterfake(tests)databaseportPostgres-style DBadapterlivein-memoryadapterfake(tests)receiptsstorageportS3-styleobjectstorageadapterlivein-memoryadapterfake(tests)emailportemail APIadapterlivein-memoryadapterfake(tests)entry point(main)ADAPTERS · outboundwhat you callPort = a socket the core owns.Adapter = a plug that fits it.callsimplementsbuilds & passes in
Production: the rule runs against the live plugs, the real payment API, database, storage and email.Tests: the same rule runs against in-memory fakes. Nothing leaves the machine. (A fifth port, the PDF maker, is left out of the picture.)
off: production, live plugs

The sockets domain/ports.ts

export interface PaymentGateway {
  charge(req: { card: string; amountCents: number }): Promise<{ chargeId: string }>;
}
export interface OrderStore {
  save(order: Order): Promise<void>;
}
export interface ReceiptStorage {
  put(key: string, body: Uint8Array, contentType: string): Promise<{ url: string }>;
}
export interface ReceiptMailer {
  sendReceipt(msg: { to: string; orderId: string; receiptUrl: string }): Promise<void>;
}

One fake plug adapters/fakes.ts

export class FakePayments implements PaymentGateway {
  charges: { card: string; amountCents: number }[] = [];
  fail = false;

  async charge(req: { card: string; amountCents: number }): Promise<{ chargeId: string }> {
    if (this.fail) throw new Error("card declined");
    this.charges.push({ card: req.card, amountCents: req.amountCents });
    return { chargeId: `ch_${this.charges.length}` };
  }
}

Tests, with the fakes 11 of 11 pass, offline

$ node --experimental-strip-types --test tests/order.test.ts
✔ rejects a non-positive quantity before charging anything (1.666014ms)
✔ charges the card for quantity times unit price (0.991947ms)
✔ saves the order after a successful charge (0.255326ms)
✔ stores the PDF receipt in object storage and emails its link (0.411705ms)
✔ a declined card saves nothing and sends no receipt (0.236848ms)
✔ payment API: POSTs the charge with bearer auth and returns the charge id (0.44915ms)
✔ payment API: a non-2xx answer is an error (0.265312ms)
✔ order API: POSTs the order as JSON (0.254834ms)
✔ S3-style storage: PUTs the PDF bytes with its content type (0.479142ms)
✔ email API: POSTs the receipt link to the recipient (0.632305ms)
✔ PDF renderer: produces a PDF that names the order and its line (1.407329ms)
ℹ tests 11
ℹ pass 11
ℹ fail 0

Production wiring, dry run prints what it would send

$ node --experimental-strip-types main.ts
would send POST https://pay.example.test/v1/charges
would send POST https://db.example.test/orders
would send PUT https://receipts-example.s3.us-east-1.amazonaws.com/receipts/ORD-ch_dry_1.pdf
would send POST https://mail.example.test/v3/send
{
  orderId: 'ORD-ch_dry_1',
  chargeId: 'ch_dry_1',
  receiptUrl: 'https://receipts-example.s3.us-east-1.amazonaws.com/receipts/ORD-ch_dry_1.pdf'
}

And MM3 checked it

Same nine yes/no questions, same locked check as everywhere on this page, now on a five-file app with five ports. The second copy is the first one with a single change, in a folder with no label: the order rule builds the live payment adapter itself and calls it, instead of using its port.

PASSthe app as built · run MM3-0387 · overall goal 0.97

The business rule keeps to itself

0.98

Q1. Does the order rule in the domain folder itself reject a non-positive quantity?

0.98

Q2. Is the order rule in the domain folder free of any import of an adapter module?

0.93

Q3. Does the order rule in the domain folder import only from the domain or the standard library?

All nine pass; the lowest is 0.92.

FAILthe broken copy · run MM3-0388 · overall goal 0.08

The business rule keeps to itself

0.98

Q1. Does the order rule in the domain folder itself reject a non-positive quantity?

0.03

Q2. Is the order rule in the domain folder free of any import of an adapter module?

0.05

Q3. Does the order rule in the domain folder import only from the domain or the standard library?

Severity of the worst dependency: critical. The other questions mostly hold (lowest 0.83).

Honestly: one run each, so no measure of noise. We wrote the broken copy by hand. The live plugs were never run against real services, and the storage plug does not sign its requests yet. The app’s own tests (11 of 11 pass) are the evidence for what runs; MM3’s check is the evidence for how it is laid out.

9 · Passed?

Good code passes, every broken copy fails, in all six languages

What we didPassed?
Good code is accepted in all six languages✅ pass in 6 of 6 MM3-0101 · 0102 · 0103 · 0104 · 0106 · 0371
A copy with the business rule reaching into the database side is caught✅ fail in 6 of 6 MM3-0373 · 0374 · 0375 · 0376 · 0378 · 0380
Total12 of 12 as expected: six good copies pass, six broken copies fail

Run ids are small on purpose: each one is a real, recorded MM3 run.

Why it matters

If a coding agent writes your code, you need an answer you can read, not a feeling. One sentence got you the architecture; one check tells you if it holds, in whatever language you use.

Reality check

What we tested
One sentence in six languages, and a real five-port order service, all judged by one locked check.
What should work
Any project where the business rules live in one place and reach the outside only through interfaces.
What we learned the hard way
  • Our first broken copies gave the answer away: a comment named the mistake. We removed it and re-ran — still caught, 6 of 6, a little less sure.
  • One rule is weak: “business rules may not import outside libraries” was missed once, in Go (MM3-0386). We’re sharpening it.
Not yet
A real open-source project we didn’t write. That is next.
Receipts
  • Runs: the live Rust pair MM3-0371 pass and MM3-0380 fail. Good copies MM3-0101 to MM3-0104, MM3-0106 and MM3-0371. Broken copies MM3-0373 to MM3-0376, MM3-0378 and MM3-0380. The integration: MM3-0387 pass and MM3-0388 fail.
  • Go, Java and C# were not compiled here. Python and Java question wording was adjusted after reading those apps, then locked.
  • We made the broken copies ourselves, in tiny apps that look alike.
  • One run per app, so there is no measure of run-to-run noise. Cost is estimated from tokens, from the ledger.

Sources

All stories