DEV Community

Harper Xu
Harper Xu

Posted on

Draw the Failure Domains Before You Generate

You do not start generation with a prompt. You start with a map of failure domains. The generated patch must arrive last, never first.

Think of a hallway whiteboard after a busy lunch. Useful sketches appear there during every afternoon standup. Rain and passing strangers will erase them overnight.

Your architecture is the load-bearing office wall. It does not live on that public board. You pin it locally before any remote session starts.

A cheap agent will invent the missing walls. It fills every silence with confident extra plumbing. You stop that by naming every domain in writing.

Four rooms and one locked door

Call your local repository Domain A, always. Secrets, ADRs, and constraint files sleep there. Nothing generated may write that room directly.

Call the hashed session pack Domain B. It is a hashed and read-only outbound bundle. You copy it outward and never import it back as law.

Call the remote sketch host Domain C. Untrusted models run there without your supervision. Treat every remote file as public weather.

Call the inbound patch under test Domain D. Tests living in Domain A must judge it. Merge happens only after those tests pass.

Data flows one way through those rooms. Domain A writes Domain B before anything else. You carry B into C, then C emits D for A.

Domain A accepts D or burns it. Domain C never becomes the system of record. The arrow has no legal reverse path.

If you reverse that arrow, the hallway owns the building. The whiteboard then starts approving load-bearing changes. That is how quiet architecture dies at work.

Pin the map in a file

Do not keep the map in chat history. Chat history is only Domain C weather. Put the map in version control beside the code.

Here is a small constraint file you can actually run against. Commit it beside the code, not inside the chat.

# arch/constraints.yaml
system: checkout-api
version: 1
owner: local-repo
data_flow:
  inbound: "https request -> parser -> domain -> db"
  outbound: "domain -> http adapter -> caller"
forbidden:
  - generated modules importing arch.constraints
  - network calls from arch.domain
  - persistence inside generated sketch folders
failure_domains:
  A: local constraint tree
  B: hashed session pack
  C: remote sketch host
  D: inbound patch under test
touch_allowlist:
  - src/generated/**
  - tmp/sketch/**
never_touch:
  - arch/**
  - secrets/**
  - tests/test_arch_contracts.py
Enter fullscreen mode Exit fullscreen mode

That constraint file is boring on purpose. Boring files survive a tired architecture review. Exciting prompts usually do not survive contact.

Hash the pack before it leaves

You should not paste the whole repository into Domain C. You pack a one-way brief instead of the tree. The brief carries the hash of the constraints, not the keys.

# scripts/pack_session.py
from __future__ import annotations

import hashlib
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
CONSTRAINTS = ROOT / "arch" / "constraints.yaml"
OUT = ROOT / "tmp" / "session_pack.md"


def sha256_bytes(data: bytes) -> str:
    return hashlib.sha256(data).hexdigest()


def main() -> None:
    raw = CONSTRAINTS.read_bytes()
    digest = sha256_bytes(raw)
    brief = "\n".join(
        [
            "# Session pack (Domain B)",
            "",
            f"constraints_sha256: {digest}",
            "direction: A -> B -> C -> D -> A",
            "rule: Domain C cannot approve merges.",
            "rule: Do not edit arch/ or secrets/.",
            "rule: Emit a patch limited to touch_allowlist.",
            "",
            "```

yaml",
            raw.decode("utf-8").rstrip(),
            "

```",
            "",
        ]
    )
    OUT.parent.mkdir(parents=True, exist_ok=True)
    OUT.write_text(brief, encoding="utf-8")
    print(f"wrote {OUT}")
    print(f"constraints_sha256 {digest}")


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Run that packer on your local machine. Keep the digest in your local review notes. Commit the yaml, not the remote tree.

python scripts/pack_session.py
git add arch/constraints.yaml
git status --short
Enter fullscreen mode Exit fullscreen mode

If the digest changes, the architecture itself changed. The remote agent did not get a vote.

Contract tests are the locked door

A review comment is not a locked door. A failing contract test is the locked door. Domain A must refuse Domain D when the patch crosses a wall.

# tests/test_arch_contracts.py
from __future__ import annotations

import hashlib
import re
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
CONSTRAINTS = ROOT / "arch" / "constraints.yaml"
GENERATED = ROOT / "src" / "generated"
ARCH = ROOT / "arch"


def constraints_hash() -> str:
    return hashlib.sha256(CONSTRAINTS.read_bytes()).hexdigest()


def test_constraints_file_is_present() -> None:
    text = CONSTRAINTS.read_text(encoding="utf-8")
    assert "failure_domains" in text
    assert "never_touch" in text
    assert "touch_allowlist" in text
    assert len(constraints_hash()) == 64


def test_generated_code_stays_in_its_room() -> None:
    if not GENERATED.exists():
        return
    banned = re.compile(r"from\s+arch(\.| import)|import\s+arch")
    for path in GENERATED.rglob("*.py"):
        body = path.read_text(encoding="utf-8")
        assert banned.search(body) is None, path


def test_arch_tree_has_no_network_clients() -> None:
    needle = re.compile(r"\b(requests|httpx|socket|urllib)\b")
    for path in ARCH.rglob("*.py"):
        body = path.read_text(encoding="utf-8")
        assert needle.search(body) is None, path


def test_patch_did_not_touch_the_map() -> None:
    # Label: proposal for a CI hook. Wire to `git diff --name-only`.
    touched = []  # fill from CI, not from the sketch host
    for path in touched:
        assert not path.startswith("arch/"), path
        assert not path.startswith("secrets/"), path
Enter fullscreen mode Exit fullscreen mode

Run the door before you read the prose of the patch. Look at file names before you look at style.

python -m pytest tests/test_arch_contracts.py -q
git diff --name-only main...HEAD
Enter fullscreen mode Exit fullscreen mode

You are not reviewing style first today. You are reviewing the boundary, not the adjectives. Style review can wait for a human later.

Review flow, then arrows, then the diff

Architecture review follows a boring fixed sequence. You name constraints, then data flow, then failure domains. Only then do you read the generated diff.

Data flow is a sentence with arrows, not a diagram dump. Inbound request hits the parser, then the domain. Keep that data-flow sentence in the yaml.

The domain talks only to storage ports. Adapters talk outbound and must stay replaceable. Generated code may not invent new data arrows.

If a patch adds a hidden outbound hop, the map is already lying. Your job is to catch the new hop early. The diff is evidence, not the decision.

Where a free sketch host actually fits

You still need a place to sketch freely. Local laptops overheat during long generation loops. Context windows also forget the load-bearing wall.

A disposable host may sit in Domain C. Do not let it hold the constraint file. Weather belongs there, and walls do not.

Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode is an open source coding assistant project. It offers free model access and a free server option.

Use that option as Domain C weather, not as Domain A ground. Feed that host the session pack only. Ask for a patch against the allowlist.

Pull the resulting patch into Domain D. Let the contract tests speak before merge. If they fail, discard the host files without mourning.

Do not ask the model to remember your architecture. Memory on that host is a hallway rumor. The local yaml file is the wall.

Failure modes you should expect

The model will assume a missing adapter exists. That is Domain C filling the silence again. Your inbound parser test should fail closed, not open.

The remote tree will rot while you drink coffee. That rot is weather, not a release. Re-pack from Domain A instead of rebasing on C.

Someone will paste a .env into the sketch host. That paste crosses every domain at once. Rotate the secret and then shorten the pack.

Contract tests can lag the real boundary. A new network client sneaks in through a shell-out. Add a grep for subprocess in the next review.

Who should not use this flow

Do not use a public sketch host with regulated data. Domain C is not your compliance boundary. Keep that work on machines you already control.

Do not use this flow if Domain A is empty. Constraints that do not exist cannot travel. Write the yaml first, even if it is ugly.

Do not use this flow to skip a human architecture review. Tests catch crossings but miss a wrong domain model. A wrong wall, well painted, still falls.

What you should change next

Sign the constraint hash with a local key. A bare sha256 proves integrity, not authorship. Next, your CI should reject unconstrained architecture edits.

Publish a touch-allowlist as a machine file. Parsers beat prose during tired Friday reviews. Then record each accepted patch as a one-line ADR.

If the sketch host must persist anything, persist the digest only. Never persist the working tree out there. The hallway may keep a postcard, not blueprints.

You now have a review method, not a prompt trick. Draw the rooms and then lock the door. Generate last, only after the map exists.

You can practice Domain C on a disposable host. MonkeyCode's free server option can fill that room. Keep Domain A on your own machine.

Top comments (0)