<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Sasha Podlesniuk</title>
    <description>The latest articles on Forem by Sasha Podlesniuk (@sasha_podles).</description>
    <link>https://forem.com/sasha_podles</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3682680%2Faf5ea0f4-e046-41ef-998c-4a888837c75c.png</url>
      <title>Forem: Sasha Podlesniuk</title>
      <link>https://forem.com/sasha_podles</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/sasha_podles"/>
    <language>en</language>
    <item>
      <title>Claude Code: Using Hooks for Guaranteed Context Injection</title>
      <dc:creator>Sasha Podlesniuk</dc:creator>
      <pubDate>Mon, 02 Feb 2026 09:08:53 +0000</pubDate>
      <link>https://forem.com/sasha_podles/claude-code-using-hooks-for-guaranteed-context-injection-2jg</link>
      <guid>https://forem.com/sasha_podles/claude-code-using-hooks-for-guaranteed-context-injection-2jg</guid>
      <description>&lt;p&gt;When configuring Claude Code, most teams use &lt;code&gt;CLAUDE.md&lt;/code&gt; or skills files to define coding standards. These work well for general guidance, but they share a limitation: &lt;strong&gt;the model decides when to read them&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This post explores Claude Code hooks—a mechanism that guarantees your critical rules are injected into context before every file operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Context Loading Problem
&lt;/h2&gt;

&lt;p&gt;Vercel's engineering team ran into this with their AI agent evaluations. In their &lt;a href="https://vercel.com/blog/agents-md-outperforms-skills-in-our-agent-evals" rel="noopener noreferrer"&gt;research comparing skills vs embedded documentation&lt;/a&gt;, they found that skills-based retrieval was &lt;strong&gt;skipped in 56% of eval cases&lt;/strong&gt;. The agent simply didn't invoke them.&lt;/p&gt;

&lt;p&gt;Passive context (like &lt;code&gt;CLAUDE.md&lt;/code&gt;) performed better because it's always present. But even passive context has limits—it's read once at session start, and the model still decides how much attention to give it during each task.&lt;/p&gt;

&lt;p&gt;For rules that &lt;strong&gt;must&lt;/strong&gt; be followed every time—security practices, architectural constraints—we need something stronger.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claude Code Hooks: Guaranteed Execution
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.anthropic.com/en/docs/claude-code/hooks" rel="noopener noreferrer"&gt;Hooks&lt;/a&gt; are shell commands that execute automatically at specific points in Claude Code's lifecycle. The key difference: &lt;strong&gt;hooks run on every matching tool call, regardless of what the model thinks is relevant&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;When It Loads&lt;/th&gt;
&lt;th&gt;Reliability&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CLAUDE.md&lt;/td&gt;
&lt;td&gt;Session start&lt;/td&gt;
&lt;td&gt;Read once, attention varies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Skills&lt;/td&gt;
&lt;td&gt;When model invokes&lt;/td&gt;
&lt;td&gt;May be skipped by the model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hooks&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Every matching tool call&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Guaranteed execution&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;PreToolUse&lt;/code&gt; hook fires before Claude executes any tool—including &lt;code&gt;Read&lt;/code&gt;, &lt;code&gt;Edit&lt;/code&gt; and &lt;code&gt;Write&lt;/code&gt;. This lets you inject rules directly into context at the moment they matter most.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User: "Add a new endpoint"
    ↓
Claude prepares Read tool call
    ↓
PreToolUse hook fires ← Guaranteed, every time
    ↓
Your rules injected into context
    ↓
Claude writes code with rules present
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Implementation: Package-Aware Rule Injection
&lt;/h2&gt;

&lt;p&gt;Consider a monorepo where different packages have different coding standards. The API package requires Zod validation and parameterized SQL. The UI package mandates React Query over raw &lt;code&gt;useEffect&lt;/code&gt; fetching.&lt;/p&gt;

&lt;h3&gt;
  
  
  Project Structure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;project/
├── .claude/
│   ├── settings.json
│   └── hooks/
│       └── inject-rules.py
└── packages/
    ├── api/
    │   └── RULES.md
    └── ui/
        └── RULES.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Package Rules
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;packages/api/RULES.md&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# API Package Rules&lt;/span&gt;

&lt;span class="gu"&gt;## Required Patterns&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; All endpoints must have input validation using Zod schemas
&lt;span class="p"&gt;-&lt;/span&gt; Wrap async handlers with error middleware
&lt;span class="p"&gt;-&lt;/span&gt; Use &lt;span class="sb"&gt;`logger.info/error`&lt;/span&gt; instead of console.log
&lt;span class="p"&gt;-&lt;/span&gt; All database queries must use parameterized statements

&lt;span class="gu"&gt;## Naming Conventions&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Controllers: &lt;span class="sb"&gt;`*.controller.ts`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Routes: &lt;span class="sb"&gt;`*.routes.ts`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Validators: &lt;span class="sb"&gt;`*.schema.ts`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;packages/ui/RULES.md&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# UI Package Rules&lt;/span&gt;

&lt;span class="gu"&gt;## Required Patterns&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Use React Query for data fetching (no useEffect + fetch)
&lt;span class="p"&gt;-&lt;/span&gt; Components must be functional with TypeScript props interface
&lt;span class="p"&gt;-&lt;/span&gt; Always handle loading and error states

&lt;span class="gu"&gt;## Naming Conventions&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Components: PascalCase directories with index.tsx
&lt;span class="p"&gt;-&lt;/span&gt; Hooks: &lt;span class="sb"&gt;`use*.ts`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Styles: &lt;span class="sb"&gt;`*.module.css`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Hook Implementation
&lt;/h3&gt;

&lt;p&gt;The hook reads the file path from stdin, detects the package, and injects the corresponding rules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;.claude/hooks/inject-rules.py&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="n"&gt;input_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;file_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file_path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;PACKAGE_RULES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/packages/api/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;packages/api/RULES.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/packages/ui/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;packages/ui/RULES.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pkg_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rules_path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;PACKAGE_RULES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pkg_path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;package&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pkg_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;rules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rules_path&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hookSpecificOutput&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hookEventName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PreToolUse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;additionalContext&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;package&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] rules:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}))&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Configuration
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;.claude/settings.json&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PreToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Read|Edit|Write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"python3 .claude/hooks/inject-rules.py"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;matcher&lt;/code&gt; uses regex—&lt;code&gt;Read|Edit|Write&lt;/code&gt; captures both file editing and creation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;With this configuration, requesting a new API endpoint produces code that follows all injected rules:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Request:&lt;/strong&gt; "Add a createUser endpoint to packages/api/users.controller.ts"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;logger&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../shared/logger&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;asyncHandler&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../middleware/errorHandler&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createUserSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;asyncHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createUserSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Creating user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;th&gt;Applied&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Zod schema validation&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;asyncHandler wrapper&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;logger instead of console.log&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parameterized SQL query&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;While the same outcome can be achieved with skills, a growing skills library or codebase increases contextual noise and raises the risk that the model overlooks the correct skill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advanced Patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Compress Rules for Context Efficiency
&lt;/h3&gt;

&lt;p&gt;Every hook injection adds to the context window. As context length increases, LLM performance can drop—a phenomenon called lost in the middle. Compression isn't just about cost; it's about effectiveness.&lt;/p&gt;

&lt;p&gt;Borrowing techniques from &lt;a href="https://www.microsoft.com/en-us/research/blog/llmlingua-innovating-llm-efficiency-with-prompt-compression/" rel="noopener noreferrer"&gt;prompt compression research&lt;/a&gt;, here's how to minimize your rules footprint:&lt;/p&gt;

&lt;h4&gt;
  
  
  Technique 1: Remove Filler Words
&lt;/h4&gt;

&lt;p&gt;Strip articles (a, the), hedging words (should, please, always), and redundant phrases.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"All endpoints must have input validation"&lt;/td&gt;
&lt;td&gt;"endpoints: validate input"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"You should always use parameterized queries"&lt;/td&gt;
&lt;td&gt;"use parameterized queries"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Please make sure to wrap handlers"&lt;/td&gt;
&lt;td&gt;"wrap handlers"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Technique 2: Use Symbols and Shorthand
&lt;/h4&gt;

&lt;p&gt;Replace words with universally understood symbols—similar to how minified JS uses short variable names.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;→&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;leads to, use, becomes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;!=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;not, avoid, don't use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;✓&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;required, do this&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;✗&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;forbidden, never&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;wildcard (any)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$1, $2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;parameters&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Technique 3: Code-Like Syntax
&lt;/h4&gt;

&lt;p&gt;LLMs parse structured formats efficiently. Use patterns they recognize from code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# BEFORE&lt;/span&gt;
&lt;span class="gu"&gt;## API Package Rules&lt;/span&gt;

&lt;span class="gu"&gt;### Required Patterns&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; All endpoints must have input validation using Zod schemas
&lt;span class="p"&gt;-&lt;/span&gt; Always wrap async handlers with error middleware
&lt;span class="p"&gt;-&lt;/span&gt; Use &lt;span class="sb"&gt;`logger.info/error`&lt;/span&gt; instead of console.log for all logging
&lt;span class="p"&gt;-&lt;/span&gt; All database queries must use parameterized statements to prevent SQL injection

&lt;span class="gu"&gt;### Naming Conventions&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Controllers should be named: &lt;span class="sb"&gt;`*.controller.ts`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Routes should be named: &lt;span class="sb"&gt;`*.routes.ts`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Validators should be named: &lt;span class="sb"&gt;`*.schema.ts`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# AFTER
[api] validate:zod | wrap:asyncHandler | log:logger.* !=console.* | sql:parameterized($1,$2)
naming: *.controller.ts, *.routes.ts, *.schema.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Technique 4: Relevance Filtering
&lt;/h4&gt;

&lt;p&gt;Not every rule applies to every file. Filter dynamically in your hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Only inject SQL rules for database-related files
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;repository&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;file_path&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;rules&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;sql: parameterized queries only&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compressed format remains fully interpretable by Claude while using a fraction of the context space.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preserve Through Compaction
&lt;/h3&gt;

&lt;p&gt;Use &lt;code&gt;PreCompact&lt;/code&gt; to ensure critical rules survive context compaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"PreCompact"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"python3 .claude/hooks/inject-critical-rules.py"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Debugging
&lt;/h3&gt;

&lt;p&gt;Run &lt;code&gt;claude --debug&lt;/code&gt; to see hook execution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;DEBUG] Executing hooks &lt;span class="k"&gt;for &lt;/span&gt;PreToolUse:Write
&lt;span class="o"&gt;[&lt;/span&gt;DEBUG] Hook &lt;span class="nb"&gt;command &lt;/span&gt;completed with status 0: &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"hookSpecificOutput"&lt;/span&gt;:...&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create &lt;code&gt;.claude/settings.json&lt;/code&gt; with the PreToolUse configuration&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;.claude/hooks/inject-rules.py&lt;/code&gt; with your package detection logic&lt;/li&gt;
&lt;li&gt;Create &lt;code&gt;RULES.md&lt;/code&gt; files in each package directory&lt;/li&gt;
&lt;li&gt;Restart Claude Code to load the new hooks&lt;/li&gt;
&lt;li&gt;Edit a file and observe the injected context with &lt;code&gt;claude --debug&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>programming</category>
    </item>
    <item>
      <title>From Prompts to Invariants: Re-architecting Systems with ArchUnit and LLMs</title>
      <dc:creator>Sasha Podlesniuk</dc:creator>
      <pubDate>Thu, 18 Dec 2025 13:40:56 +0000</pubDate>
      <link>https://forem.com/sasha_podles/from-prompts-to-invariants-re-architecting-systems-with-archunit-and-llms-1n50</link>
      <guid>https://forem.com/sasha_podles/from-prompts-to-invariants-re-architecting-systems-with-archunit-and-llms-1n50</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F38l7kmfclv61xegez9bh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F38l7kmfclv61xegez9bh.png" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Architectural drift is a slow tax on every codebase.&lt;/p&gt;

&lt;p&gt;Layers blur. Boundaries erode. Patterns decay. Over time, what started as intentional design becomes accidental structure.&lt;/p&gt;

&lt;p&gt;AI coding agents and autonomous loops promise a way out: give the agent a task, let it iterate, and stop when it “looks done”. This works well for local refactors and mechanical changes.&lt;/p&gt;

&lt;p&gt;But architectural change is different.&lt;/p&gt;

&lt;p&gt;Architecture is not a one-off transformation.&lt;br&gt;&lt;br&gt;
It is a target state defined by invariants.&lt;/p&gt;

&lt;p&gt;What if we could run an autonomous loop, but instead of relying on natural language prompts to guide it, we defined the desired architectural outcome as executable constraints?&lt;/p&gt;

&lt;p&gt;That is where ArchUnit becomes the control mechanism for autonomous architectural change.&lt;/p&gt;
&lt;h3&gt;
  
  
  Reframing Autonomous Loops
&lt;/h3&gt;

&lt;p&gt;In the &lt;a href="https://github.com/anthropics/claude-code/tree/main/plugins/ralph-wiggum" rel="noopener noreferrer"&gt;&lt;em&gt;Ralph Wiggum autonomous loop&lt;/em&gt;&lt;/a&gt; technique — a lightweight pattern for running LLM agents in self-correcting cycles — an agent repeatedly iterates on a task with minimal human intervention. The loop typically looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Give the agent a task description&lt;/li&gt;
&lt;li&gt;Let it modify the code&lt;/li&gt;
&lt;li&gt;Evaluate the result (tests, output, heuristics)&lt;/li&gt;
&lt;li&gt;Repeat until a stop condition is met&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The weak point is step 3.&lt;/p&gt;

&lt;p&gt;For architectural tasks, &lt;em&gt;“did it work?”&lt;/em&gt; is rarely a binary answer. The agent must infer intent from prompts and guess whether the desired architectural state has been reached.&lt;/p&gt;

&lt;p&gt;We can keep the loop, but replace the stop condition.&lt;/p&gt;

&lt;p&gt;Instead of asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Does this look architecturally correct?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;we ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Do the architectural tests pass?&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When the stop condition is defined by ArchUnit tests, the loop becomes deterministic.&lt;/p&gt;
&lt;h3&gt;
  
  
  Architecture as the Loop Condition
&lt;/h3&gt;

&lt;p&gt;In this model, ArchUnit tests define the &lt;strong&gt;desired architectural change&lt;/strong&gt; , not just static restrictions.&lt;/p&gt;

&lt;p&gt;The loop looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write an ArchUnit test that expresses the target architecture&lt;/li&gt;
&lt;li&gt;Run the test suite (it fails)&lt;/li&gt;
&lt;li&gt;Ask the agent to make the tests pass&lt;/li&gt;
&lt;li&gt;Repeat until all architectural tests are green&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is an autonomous loop, but one that converges on explicit invariants rather than emergent behavior.&lt;/p&gt;

&lt;p&gt;The agent is not discovering architecture.&lt;br&gt;&lt;br&gt;
It is satisfying constraints.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why This Is Different from “Using ArchUnit for Rules”
&lt;/h3&gt;

&lt;p&gt;This is not just about enforcing rules after the fact.&lt;/p&gt;

&lt;p&gt;The key shift is &lt;strong&gt;using ArchUnit tests as the primary driver of change&lt;/strong&gt; :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The test describes &lt;em&gt;where the system should go&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Failures show &lt;em&gt;what still violates the target state&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Passing tests mean &lt;em&gt;the architectural migration is complete&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, the ArchUnit test is the prompt, the feedback mechanism, and the stop condition.&lt;/p&gt;
&lt;h3&gt;
  
  
  Example 1: Driving a Shift from Unit Tests to Integration Tests
&lt;/h3&gt;

&lt;p&gt;Suppose you want to move from unit tests with mocked repositories to integration tests.&lt;/p&gt;

&lt;p&gt;Instead of telling an agent:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“Refactor tests to stop mocking repositories”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You encode the desired end state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;repositoriesShouldNotBeMockedInTests&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;JavaClasses&lt;/span&gt; &lt;span class="n"&gt;importedClasses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ClassFileImporter&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;importPackages&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"com.example.demo"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;that&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;areAnnotatedWith&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;mockito&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Mock&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;should&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;haveRawType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;not&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resideInAPackage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"..repository.."&lt;/span&gt;&lt;span class="o"&gt;)))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;because&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"repositories should not be mocked - use integration tests instead"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;allowEmptyShould&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;check&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;importedClasses&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test initially fails.&lt;/p&gt;

&lt;p&gt;Now the autonomous loop is trivial:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run tests&lt;/li&gt;
&lt;li&gt;See the failure&lt;/li&gt;
&lt;li&gt;Ask the agent to fix the violations&lt;/li&gt;
&lt;li&gt;Repeat&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The loop ends naturally when no repository mocks remain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example 2: Migrating Stripe Usage with a Test-Driven Loop
&lt;/h3&gt;

&lt;p&gt;Consider a system that uses Stripe’s static API configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Stripe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"sk_test_123"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="nc"&gt;Charge&lt;/span&gt; &lt;span class="n"&gt;charge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Charge&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You want to migrate to explicit client usage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;StripeClient&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StripeClient&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sk_test_123"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;v1&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;charges&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of instructing the agent to “refactor Stripe usage”, you define the invariant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;stripeStaticApiCallsMustNotBeUsed&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;JavaClasses&lt;/span&gt; &lt;span class="n"&gt;importedClasses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ClassFileImporter&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;importPackages&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"com.example.demo"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="nc"&gt;DescribedPredicate&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;JavaCall&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;?&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;isStripeStaticCall&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="nc"&gt;DescribedPredicate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;describe&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"static method in com.stripe.model (excluding builder())"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
                &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTarget&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getOwner&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getPackage&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;startsWith&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"com.stripe.model"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTarget&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;resolveMember&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;member&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;member&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getModifiers&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;STATIC&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orElse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTarget&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;equals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"builder"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;noClasses&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;should&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;callCodeUnitWhere&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isStripeStaticCall&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;because&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Stripe static API calls should not be used - inject StripeClient instead"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;check&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;importedClasses&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test now &lt;em&gt;defines the migration target&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The autonomous loop runs until every static Stripe call is eliminated.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the Agent Actually Does
&lt;/h3&gt;

&lt;p&gt;From the agent’s perspective, this is ideal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Failures point directly to violating code&lt;/li&gt;
&lt;li&gt;because(...) explains the expected fix&lt;/li&gt;
&lt;li&gt;The search space narrows with every iteration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is no need for the agent to understand Stripe, testing philosophy, or architectural intent in abstract terms.&lt;/p&gt;

&lt;p&gt;It only needs to make the build green.&lt;/p&gt;

&lt;h3&gt;
  
  
  Benefits of Test-Driven Autonomous Architecture
&lt;/h3&gt;

&lt;p&gt;Using ArchUnit as the loop condition gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deterministic stopping criteria&lt;/li&gt;
&lt;li&gt;Predictable convergence&lt;/li&gt;
&lt;li&gt;Clear auditability for human reviewers&lt;/li&gt;
&lt;li&gt;Architecture that stays enforced after the migration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most importantly, the architecture does not live in prompts or documentation.&lt;br&gt;&lt;br&gt;
It lives in executable tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Closing Thought
&lt;/h3&gt;

&lt;p&gt;Autonomous loops are powerful, but architecture requires precision.&lt;/p&gt;

&lt;p&gt;By defining the desired architectural change as ArchUnit tests, we can keep the loop while removing ambiguity. The agent iterates, the tests judge, and the system converges on a clearly defined target state.&lt;/p&gt;

&lt;p&gt;This approach is not without trade-offs. Architectural invariants can be over-constrained, hard to express, or temporarily at odds with incremental change. Poorly designed tests can block valid evolution just as easily as they can prevent drift. The discipline shifts from writing better prompts to designing better constraints — and those constraints still require human judgment.&lt;/p&gt;

&lt;p&gt;This is not replacing human architectural thinking.&lt;br&gt;&lt;br&gt;
 It is making architectural intent explicit, reviewable, and enforceable — so machines can apply it reliably, and humans can reason about it clearly.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>architecture</category>
      <category>ai</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Making Coding Agent Verification Fast, Accurate, and Lightweight with Architectural Tests</title>
      <dc:creator>Sasha Podlesniuk</dc:creator>
      <pubDate>Mon, 08 Dec 2025 14:59:58 +0000</pubDate>
      <link>https://forem.com/sasha_podles/making-coding-agent-verification-fast-accurate-and-lightweight-with-architectural-tests-1a3p</link>
      <guid>https://forem.com/sasha_podles/making-coding-agent-verification-fast-accurate-and-lightweight-with-architectural-tests-1a3p</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzmlu18zvb8m8zaeobjfy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzmlu18zvb8m8zaeobjfy.png" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you build software with the help of an LLM, your workflow naturally splits into two complementary phases: &lt;strong&gt;guiding&lt;/strong&gt; and &lt;strong&gt;checking&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Guiding&lt;/strong&gt; is all about context-setting. You feed the model prompts, examples, architectural rules, and any tribal knowledge your team has accumulated over the years. You’re essentially teaching the LLM &lt;em&gt;how things should be done&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checking&lt;/strong&gt; is where you verify the model’s output through human review, automated guardrails, or even an additional “verification prompt” that asks the LLM to critique its own work.&lt;/p&gt;

&lt;p&gt;Today, let’s zoom in on this second phase and explore how it can help balance an entire AI-assisted development workflow by using architectural unit tests.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Architectural unit tests help ensure that a project is built in a consistent, sensible way. Instead of testing what the code does, they check that the code is put in the right folders, depends on the right things, and follows the patterns the team agreed on.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  A Typical Setup: Layers, Rules, and the Chaos Between Them
&lt;/h3&gt;

&lt;p&gt;Imagine a fairly standard architecture inside your org:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Controllers, Events, Background Jobs:&lt;/strong&gt; Your main entry points.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application &amp;amp; Domain layers:&lt;/strong&gt; Where all your business logic lives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infra / Repositories / Persistence&lt;/strong&gt; : The nuts and bolts behind the scenes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A forest of internal rules&lt;/strong&gt; : Dependency directions, DTO requirements, guidance on connecting controllers, and dozens of conventions born from real-world constraints.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the system your LLM must navigate, consistently, predictably, and without going rogue.&lt;/p&gt;

&lt;p&gt;Naturally, the first instinct is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Let’s just write all this down.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So you add everything into a markdown file. If you’re using Claude Code, maybe it’s CLAUDE.md. Eventually you introduce something like &lt;strong&gt;skills&lt;/strong&gt; to structure that guidance better.&lt;/p&gt;

&lt;p&gt;And you know what?&lt;/p&gt;

&lt;p&gt;It &lt;em&gt;does&lt;/em&gt; work… &lt;strong&gt;at first&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But as your rulebook grows, the cracks start to show.&lt;/p&gt;

&lt;h3&gt;
  
  
  When Guidance Starts Fighting Itself
&lt;/h3&gt;

&lt;p&gt;Anyone who has used LLMs for code generation knows this moment well: the instruction you thought was perfectly clear gets ignored or worse, overwritten by another instruction you forgot you wrote.&lt;/p&gt;

&lt;p&gt;Some real examples from my setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I had a long description explaining that our application must use &lt;strong&gt;CQRS&lt;/strong&gt; (commands and queries) instead of Service classes. For example, instead of:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SomeController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;UserService&lt;/span&gt; &lt;span class="n"&gt;userService&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;userService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Should be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SomeController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Mediator&lt;/span&gt; &lt;span class="n"&gt;mediator&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;mediator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GetUserById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;I added another block clarifying that all controller responses must use &lt;strong&gt;DTOs&lt;/strong&gt; , never domain objects, for instance:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SomeController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Mediator&lt;/span&gt; &lt;span class="n"&gt;mediator&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UserDto&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;mediator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GetUserById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;)).&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;UserDto:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;fromUser&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Later, after issues popped up, I added rules forbidding cross-package communication within the application layer (no handler-to-handler commands):
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GetUserByIdHandler&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;Handler&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Mediator&lt;/span&gt; &lt;span class="n"&gt;mediator&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// NOT ALLOWED&lt;/span&gt;

  &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;mediator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AnotherCommand&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// NOT ALLOWED&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each addition fixed something… but also increased the cognitive load on both me and LLM.&lt;/p&gt;

&lt;p&gt;To make things more complex, in a typical mixed codebase, where old implementation still coexist with a new one (Service classes live together with CQRS), the model often sees both patterns and becomes unsure which one to follow.&lt;/p&gt;

&lt;p&gt;Soon you’re in a loop of:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I’ll add one more rule; that should fix it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Until the rulebook becomes a labyrinth.&lt;/p&gt;

&lt;h3&gt;
  
  
  Guidance Alone Isn’t Enough
&lt;/h3&gt;

&lt;p&gt;At some point, scaling guidance becomes expensive both in tokens and in mental overhead. What you &lt;em&gt;really&lt;/em&gt; need is a fast, deterministic way to verify output and give the LLM immediate feedback.&lt;/p&gt;

&lt;p&gt;Ideally something:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Predictable&lt;/strong&gt; (clear pass/fail)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cheap&lt;/strong&gt; (no giant prompt blocks)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quick to run&lt;/strong&gt; (so feedback can be automated)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And this is where an unexpected hero enters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using Architectural Tests as LLM Feedback Signals
&lt;/h3&gt;

&lt;p&gt;Architectural testing tools like &lt;a href="https://www.archunit.org/" rel="noopener noreferrer"&gt;ArchUnit&lt;/a&gt; were originally designed to keep software engineers from violating architectural constraints.&lt;/p&gt;

&lt;p&gt;Turns out, they are &lt;strong&gt;excellent&lt;/strong&gt; at correcting LLMs, too.&lt;/p&gt;

&lt;p&gt;Instead of endlessly describing rules in prose, you can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Encode the rule as an architectural test.&lt;/li&gt;
&lt;li&gt;Have the LLM generate code.&lt;/li&gt;
&lt;li&gt;Run the tests.&lt;/li&gt;
&lt;li&gt;Feed the failures back to the LLM.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now the model isn’t guessing what you meant; it is responding to a precise, machine enforced contract.&lt;/p&gt;

&lt;p&gt;This shifts the system from:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;LLM tries to remember everything&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;to&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;LLM generates a proposal → tests verify correctness → LLM fixes only what failed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Suddenly, your guidance becomes lighter, your rulebook becomes smaller, and your architecture becomes much easier for the model to follow.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Exactly Can We Enforce? A Practical Tour of Rules
&lt;/h3&gt;

&lt;p&gt;Now let’s look at the kinds of architectural rules we &lt;em&gt;can&lt;/em&gt; enforce and how they help us keep an LLM on the rails.&lt;/p&gt;

&lt;p&gt;For the examples below, we’ll use a Spring Boot project paired with &lt;strong&gt;ArchUnit&lt;/strong&gt;  tests.&lt;/p&gt;

&lt;p&gt;We’ll tackle the same challenges described in the earlier section.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Eliminating Service Packages (to Push CQRS)
&lt;/h3&gt;

&lt;p&gt;One of the recurring issues was the LLM drifting back toward a traditional &lt;em&gt;Service Layer&lt;/em&gt; approach often by creating a brand new service package out of thin air.&lt;/p&gt;

&lt;p&gt;A simple rule shuts that down immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="nd"&gt;@DisplayName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Classes should not reside in 'service' packages"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;noClassesInServicePackages&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;noClasses&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;should&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;resideInAPackage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"..service.."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;because&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"use 'application' package with mediator instead of 'service'"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;check&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;importedClasses&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is small but incredibly effective.&lt;/p&gt;

&lt;p&gt;If the LLM tries to “helpfully” reintroduce Service classes, this test fails instantly steering it back toward the CQRS pattern.&lt;/p&gt;

&lt;p&gt;This was reinforced with one more constraint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="no"&gt;ALLOWED_ROOT_PACKAGES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s"&gt;"..application.."&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"..controller.."&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"..config.."&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="o"&gt;};&lt;/span&gt;

&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="nd"&gt;@DisplayName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Classes must reside in allowed root-level packages only"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;classesResideInAllowedPackagesOnly&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;classes&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;should&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;resideInAnyPackage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;ALLOWED_ROOT_PACKAGES&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;because&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"enforces standard package structure: application, controller, config"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;check&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;importedClasses&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This guards the &lt;strong&gt;root package&lt;/strong&gt; from accumulating unexpected folders like utils, common, manager, or anything else the LLM might creatively invent.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Restricting the Controller Layer
&lt;/h3&gt;

&lt;p&gt;Controllers should depend on only two kinds of things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;DTOs:&lt;/strong&gt; Plain transport objects with no DI annotations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Controller classes:&lt;/strong&gt; Annotated with @RestController and extending your internal ApiController abstraction (which provides the mediator for sending commands/queries).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Anything else — domain objects, application handlers, repository types — is forbidden.&lt;/p&gt;

&lt;p&gt;You can enforce that with a rule like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="nd"&gt;@DisplayName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Controller package should only contain DTOs and RestControllers"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;controllerPackageOnlyContainsDtosAndRestControllers&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;noClasses&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;that&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;resideInAPackage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"..controller.."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;and&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;areNotAnnotatedWith&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"org.springframework.web.bind.annotation.RestController"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;should&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;notBeAnnotatedWith&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"org.springframework.stereotype.Service"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orShould&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;notBeAnnotatedWith&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"org.springframework.stereotype.Component"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orShould&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;notBeAnnotatedWith&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"org.springframework.stereotype.Repository"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;because&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"non-controller classes must be plain DTOs without DI annotations"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;check&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;importedClasses&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="nd"&gt;@DisplayName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"RestControllers must extend ApiController"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;restControllersMustExtendApiController&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;classes&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;that&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;areAnnotatedWith&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"org.springframework.web.bind.annotation.RestController"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;should&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;beAssignableTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"com.example.ApiController"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;because&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"controllers must extend ApiController to access the mediator"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;check&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;importedClasses&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This single constraint prevents the LLM from “accidentally” injecting half your system into the controller layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Enforcing CQRS in the Application Layer
&lt;/h3&gt;

&lt;p&gt;All &lt;strong&gt;public&lt;/strong&gt; classes in the application package should follow CQRS patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Commands / Queries&lt;/li&gt;
&lt;li&gt;CommandHandlers / QueryHandlers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything else should remain package-private.&lt;/p&gt;

&lt;p&gt;A rule might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="nd"&gt;@DisplayName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Public classes in application layer must implement CQRS interfaces"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;applicationLayerPublicClassesMustImplementCqrs&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;classes&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;that&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;resideInAPackage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"..application.."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;and&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;arePublic&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;and&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;areNotInterfaces&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;should&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;beAssignableTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"com.example.Command"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orShould&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;beAssignableTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"com.example.Command.Handler"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;because&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"public classes must be Commands or Handlers - catches accidental UserService or OrderManager"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;check&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;importedClasses&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This automatically catches cases where an LLM introduces a UserService or OrderManager, especially if it makes it public to expose it to controllers.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Preventing Application → Application Communication
&lt;/h3&gt;

&lt;p&gt;Another common violation: the LLM tries to send commands or queries &lt;em&gt;between&lt;/em&gt; application packages, e.g., user handlers calling invoice handlers.&lt;/p&gt;

&lt;p&gt;That breaks the vertical-slice structure.&lt;/p&gt;

&lt;p&gt;ArchUnit allows us to forbid this and immediately suggest a fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="nd"&gt;@DisplayName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Application layer should not import Pipeline"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;applicationLayerShouldNotImportPipeline&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;noClasses&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;that&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;resideInAPackage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"..application.."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;should&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;dependOnClassesThat&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;haveFullyQualifiedName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"com.example.Mediator"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;because&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Application handlers should not call other handlers via Mediator. FIX: consider using domain events"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;check&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;importedClasses&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This turns a subtle architecture rule into a crisp, enforceable contract.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Ensuring Controllers Return DTOs Only
&lt;/h3&gt;

&lt;p&gt;A bonus check validates that controller return types never leak domain entities or events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="nd"&gt;@DisplayName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Controller methods should not return domain entities or events"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;controllerMethodsShouldOnlyReturnDtos&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;ArchCondition&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nc"&gt;JavaMethod&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;notReturnDomainOrEvents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ArchCondition&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"not return domain or event types"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JavaMethod&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ConditionEvents&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getReturnType&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getAllInvolvedRawTypes&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPackageName&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;".domain."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
          &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPackageName&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;".events."&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forEach&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SimpleConditionEvent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;violated&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
          &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
          &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%s returns forbidden type %s"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getFullName&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;)));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;};&lt;/span&gt;
  &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;that&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;areDeclaredInClassesThat&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;areAnnotatedWith&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"org.springframework.web.bind.annotation.RestController"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;and&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;arePublic&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;should&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;notReturnDomainOrEvents&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;because&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"controllers must return DTOs only - prevents leaking domain internals"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;check&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;importedClasses&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This rule alone helps maintain API stability and prevents accidental exposure of internals.&lt;/p&gt;

&lt;p&gt;And of course, this is only the beginning. You can add many more invariant checks such as naming conventions, allowed dependency flows, type usage rules, and other architectural guarantees that help keep the system consistent and easier for the LLM to navigate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Real Feedback the LLM Receives
&lt;/h3&gt;

&lt;p&gt;With these tests in place, mistakes become precise, actionable feedback:&lt;/p&gt;

&lt;h3&gt;
  
  
  CASE 1: LLM creates a service package with a service class
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Feedback:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unexpected package service detected&lt;/li&gt;
&lt;li&gt;Only allowed root packages are A, B, C&lt;/li&gt;
&lt;li&gt;Service classes are forbidden, use application&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  CASE 2: LLM adds a public Service inside application
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Feedback:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Public class violates application-layer rules&lt;/li&gt;
&lt;li&gt;Internal abstractions are allowed, but public Service classes are not&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  CASE 3: LLM sends a command from one application slice to another
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Feedback:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Application → Application communication is not allowed&lt;/li&gt;
&lt;li&gt;Use Domain Events instead of cross-slice commands&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6. Preventing LLMs from Modifying the ArchUnit Tests Themselves
&lt;/h3&gt;

&lt;p&gt;If you want coding agents to work autonomously, you need to guard the tests from being “helpfully fixed” by the model.&lt;/p&gt;

&lt;p&gt;For example, a simple Claude Code hook can block edits to test files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PreToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Edit|Write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jq -r '.tool_input.file_path' | grep -qE '/architecture/.*Test&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;.java$' &amp;amp;&amp;amp; exit 2 || exit 0"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Engineers can manually update tests; the agent should not.&lt;/p&gt;

&lt;h3&gt;
  
  
  Handling Mixed Codebases with Freezing Rules
&lt;/h3&gt;

&lt;p&gt;Many teams operate in a mixed codebase: part legacy and part modern. In our case, the new approach is CQRS, while the existing implementation still uses traditional Service classes. Fixing everything at once is not realistic and exposing both implementations to the LLM creates confusion. The model sees two competing patterns and does not always pick the correct one.&lt;/p&gt;

&lt;p&gt;Freezing Rules help stabilise this situation by letting you introduce new architectural constraints without breaking existing code or overwhelming the LLM with conflicting signals.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;ArchRule&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FreezingArchRule&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;freeze&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;classes&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;should&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt; &lt;span class="cm"&gt;/* your actual ArchRule */&lt;/span&gt;
&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Putting It All Together
&lt;/h3&gt;

&lt;p&gt;This architecture-focused approach gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A tight feedback loop&lt;/li&gt;
&lt;li&gt;Deterministic, test-driven constraints&lt;/li&gt;
&lt;li&gt;Cleaner prompts (smaller CLAUDE.md / SKILLS files)&lt;/li&gt;
&lt;li&gt;A more predictable LLM-assisted workflow&lt;/li&gt;
&lt;li&gt;Less “LLM drift” into legacy patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It will not verify everything, for example ensuring that a repository query has the right index would require deeper static or runtime analysis. But it significantly reduces architectural mistakes while keeping guidance lean.&lt;/p&gt;

&lt;h3&gt;
  
  
  What’s Next?
&lt;/h3&gt;

&lt;p&gt;ArchUnit also exposes &lt;a href="https://www.archunit.org/userguide/html/000_Index.html#_software_architecture_metrics" rel="noopener noreferrer"&gt;&lt;strong&gt;metrics&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Next time, we can explore whether those metrics can help keep autonomously generated codebases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;clean,&lt;/li&gt;
&lt;li&gt;analysable,&lt;/li&gt;
&lt;li&gt;and maintainable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…for both software engineers &lt;em&gt;and&lt;/em&gt; coding agents.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>agents</category>
      <category>agenticai</category>
      <category>llm</category>
    </item>
  </channel>
</rss>
