Architektur jenseits von Layer: tsarch für AI-Agents

  1. KI-gestütztes Coding: Architektur als ausführbarer Vertrag
  2. Architektur jenseits von Layer: tsarch für AI-Agents

Im ersten Teil dieser Serie haben wir die große Leitplanke aufgebaut: Architekturregeln werden dokumentiert, über Rules und Skills in den Kontext des Coding-Agents gebracht, mit Sheriff geprüft und über Stop-Hooks als deterministisches Feedback zurückgespielt.

Damit ist die Feedback-Loop für Domänen- und Layer-Grenzen geschlossen. Bei Feature Slicing entstehen aber zusätzlich Regeln innerhalb eines Features – etwa zwischen Smart und Dumb Components, Stores sowie Services für den Datenzugriff.

Genau hier setzt dieser zweite Teil an. Wir ergänzen das Setup um tsarch, ein an die bekannte Bibliothek ArchUnit angelehntes Werkzeug für TypeScript-Projekte. Damit werden Namens- und Zugriffskonventionen zu ausführbaren Architekturregeln – geprüft als Unit-Test und eingebunden in dieselbe Agenten-Feedback-Loop, die im ersten Teil bereits Sheriff getragen hat.

Wo Layer an ihre Grenzen stoßen, übernehmen Namenskonventionen: tsarch prüft sie deterministisch und macht aus einem Architekturbruch ein maschinelles Feedback, das der Coding-Agent aufgreift.

📂 Source Code (Branch: ai-arc)

Architekturregeln über Building Blocks zusätzlich zu Layern

Im ersten Teil haben wir mit Sheriff die Kommunikation zwischen Modulen in Bahnen gelenkt: Domänen repräsentieren fachliche Grenzen, Layer technische. Allerdings lassen sich nicht alle technischen Einschränkungen gut über Layer ausdrücken – schon gar nicht, wenn wir bei Feature Slicing erlauben, dass ein Feature seine eigenen Dumb Components, Datenzugriffsservices und Stores hat. Diese Bausteine liegen dann gemeinsam im Feature-Ordner und nicht getrennt in eigenen Layern.

Deshalb schränken wir hier zusätzlich nach Building Blocks ein. Maßgeblich ist nicht mehr nur, in welchem Layer ein Baustein liegt, sondern welche Art von Baustein er ist – und welche Bausteine aufeinander zugreifen dürfen:

Building Blocks und ihre erlaubten Zugriffe: Eine Smart Component greift auf einen Store zu, der Store auf den Data-Access-Client. Alternativ geht die Smart Component über einen Coordinator, der mehrere Stores kombiniert.

Die zentrale Regel für unser Beispielprojekt lautet: Eine Smart Component greift auf einen Store zu, und nur der Store greift auf den Data-Access-Client zu. Smart Components dürfen also nicht direkt auf den Client zugreifen, und Stores dürfen nicht auf andere Stores zugreifen.

Gerade beim Einsatz von leichtgewichtigen Stores braucht es häufig einen Coordinator, der selbst keine Daten speichert, sondern für einen Use Case die Zustände aus unterschiedlichen Stores – gegebenenfalls aus unterschiedlichen Layern – bereitstellt und sie über computed-Signale miteinander verknüpft. Diese Use-Case-Steuerung übernimmt der Coordinator, der aus Sicht der Komponente wie ein „richtiger" Store aussieht. So vermeiden wir Store-zu-Store-Abhängigkeiten und damit Zyklen, ohne den Komfort einer gebündelten Sicht auf mehrere Stores aufzugeben.

Eine bewusste Ausnahme gibt es bei den Dumb Components: Sie bekommen nur Zugriff auf Stores im selben Ordner oder in Kindordnern. In diesem Fall gehen wir von lokalem State Management aus, das ein reines Implementierungsdetail der Dumb Component ist. Abgesehen davon dürfen Dumb Components nicht auf die anderen hier beschriebenen Bausteine zugreifen.

Building Blocks über Datei-Suffixe erkennen

Die einzelnen Building Blocks erkennen wir anhand von Datei-Suffixen. Das passt gut zu den aktuellen Konventionen des Angular-Teams, die von der CLI umgesetzt werden: Komponenten, Services und Direktiven bekommen keine Standardsuffixe mehr. Nicht, weil Suffixe schlecht wären, sondern weil generische Suffixe wie Component oder Service zu wenig Mehrwert bieten. Das Angular-Team hat aber ausdrücklich betont, dass man eigene, semantisch stärkere Suffixe vergeben kann – und genau das nutzen wir hier.

In unserem Demo-Projekt habe ich mich für die folgenden Suffixe entschieden:

  • Smart Components tragen einen sprechenden Use-Case-Suffix: -page, -search, -edit, -detail oder -overview – etwa flight-search.ts mit der Klasse FlightSearch oder luggage-overview.ts mit LuggageOverview.
  • Stores enden auf -store.ts – etwa flight-search-store.ts mit FlightSearchStore oder passenger-detail-store.ts mit PassengerDetailStore.
  • Coordinatoren von Stores enden auf -coordinator.ts – etwa summary-coordinator.ts mit SummaryCoordinator.
  • Datenzugriff (Clients) endet auf -client.ts – etwa flight-client.ts mit FlightClient oder airport-client.ts mit AirportClient.
  • Dumb Components erkennen wir an den Suffixen -card und -pane oder daran, dass sie in einem ui-Ordner liegen – etwa flight-card.ts.

Die maßgebliche, ausführbare Definition dieser Konventionen lebt im tsarch-Unit-Test selbst, den wir uns gleich genauer ansehen. Dort stehen die Suffixe als reguläre Ausdrücke und damit unmissverständlich – sowohl für Menschen als auch für Coding-Agents.

tsarch einrichten

tsarch parst das TypeScript-Projekt über die Compiler-API und erlaubt es, Abhängigkeiten zwischen Dateien in lesbaren, fließend formulierten Regeln zu prüfen. Wir installieren das npm-Paket als Dev-Dependency:

npm install -D tsarch

Damit tsarch weiß, welche Dateien zum Projekt gehören, braucht es eine tsconfig. Da tsarch diese aber direkt liest und dabei kein extends auflöst, müssen wir leider eine eigene, kleine tsconfig.arch.json bereitstellen, die genau das enthält, was tsarch benötigt: die relevanten Compiler-Optionen (etwa target, module, moduleResolution und experimentalDecorators für Angulars Decorators) sowie literal angegebene include- und exclude-Pfade:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "esnext",
    "moduleResolution": "bundler",
    "experimentalDecorators": true,
  },
  "include": ["src/**/*.ts"],
  "exclude": ["src/**/*.spec.ts", "src/app/testing/**", "node_modules"]
}

Schön ist diese Dopplung der tsconfig zugegebenermaßen nicht – ich hoffe, dass ein zukünftiger PR an tsarch das Auflösen von extends nachrüstet. Bis dahin ist die kleine eigene Datei ein überschaubarer Preis.

Die Architektur-Regeln selbst sind ganz normale Tests. Sie laufen jedoch nicht in der browserbasierten ng test-Umgebung, sondern mit Vitest in einem Node-Environment, weil tsarch das TypeScript-Projekt über den Compiler und das Dateisystem analysiert. Die Konfiguration dafür ist überschaubar:

import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    include: ['arch/**/*.spec.ts'],
    testTimeout: 60_000,
    hookTimeout: 60_000,
  },
});

Schließlich ergänzen wir in der package.json ein Skript, das diese Architektur-Tests startet:

"test:arch": "vitest run --config vitest.arch.config.ts"

Damit steht das Grundgerüst. Die eigentlichen Regeln liegen in arch/access-rules.spec.ts und greifen auf einige Hilfsfunktionen aus arch/utils.ts zurück. Beide sehen wir uns in den folgenden Abschnitten Regel für Regel an.

Zugriff auf Data Access einschränken

In unserer Architektur dürfen nur Stores – und der ai-Layer – auf Data-Access-Services bzw. Clients zugreifen. Alle anderen Bausteine, insbesondere Smart und Dumb Components, müssen den Umweg über einen Store nehmen. Den ai-Layer nehmen wir bewusst aus, weil er sich im Zusammenspiel mit einem Agenten zur Laufzeit um die Orchestrierung der Anwendung kümmert.

Den Kopf der Spec-Datei mit den Suffix-Konstanten und die erste Regel zeigt das folgende Snippet. Die weiteren it-Blöcke sind durch ein Auslassungszeichen ersetzt und folgen in den nächsten Abschnitten:

import { filesOfProject } from 'tsarch';
import { describe, expect, it } from 'vitest';

import {
  anyFileExcept,
  formatDependency,
  isLocalAccess,
  toDependency,
} from './utils';

const TS_CONFIG = 'tsconfig.arch.json';

const STORE = String.raw`-store\.ts$`;
const CLIENT = String.raw`-client\.ts$`;
const SMART = String.raw`-(page|search|edit|detail|overview)\.ts$`;
const DUMB = String.raw`(-(card|pane)\.ts$|/ui(-[^/]+)?/)`;
const AI_LAYER = String.raw`/ai/`;
const COORDINATOR = String.raw`-coordinator\.ts$`;

describe('architecture: suffix-based access rules', () => {
  it('only stores may access data access (clients)', async () => {
    const rule = filesOfProject(TS_CONFIG)
      .matchingPattern(anyFileExcept(STORE, AI_LAYER))
      .shouldNot()
      .dependOnFiles()
      .matchingPattern(CLIENT);

    const violations = await rule.check();
    expect(violations.map(toDependency).map(formatDependency)).toEqual([]);
  });

  // …
});

Die Suffixe definieren wir als reguläre Ausdrücke und nutzen dafür String.raw. Dieses Template-Tag liefert den String, ohne Escape-Sequenzen wie \. zu interpretieren – ein \ bleibt also ein echter Backslash und wird nicht zum Zeilenumbruch oder Ähnlichem. So sparen wir uns das doppelte Maskieren (\\. statt \.) und die regulären Ausdrücke bleiben gut lesbar.

Die Regel liest sich fast wie ein Satz: Nimm alle Dateien des Projekts, die weder ein Store noch Teil des ai-Layers sind (anyFileExcept(STORE, AI_LAYER)), und stelle sicher, dass sie nicht von Dateien abhängen, die auf -client.ts enden. rule.check() liefert eine Liste der Verstöße zurück; sind keine vorhanden, ist die erwartete Liste leer und der Test grün. Schlägt die Regel an, sehen wir dank formatDependency direkt, welche Datei unzulässig auf welchen Client zugreift.

Die im ersten it verwendeten Hilfsfunktionen aus utils.ts machen die Regel lesbar. anyFileExcept baut aus den übergebenen Suffixen ein Muster, das genau die Dateien selektiert, die keine der genannten Arten sind. Technisch wird jede Art zu einem negativen Lookahead ((?!...), also „darf nicht vorkommen"):

export function anyFileExcept(...kinds: string[]): string {
  return String.raw`^${kinds.map((kind) => `(?!.*${kind})`).join('')}.*\.ts$`;
}

anyFileExcept(STORE, AI_LAYER) erzeugt damit ein Muster, das jede .ts-Datei trifft, die weder ein Store ist noch im ai-Layer liegt. So formulieren wir „alle außer …" deklarativ, ohne jeden erlaubten Fall einzeln aufzuzählen.

Ein konkretes Beispiel macht das greifbar: anyFileExcept(STORE, AI_LAYER) setzt für jedes übergebene Positiv-Muster einen negativen Lookahead vor das eigentliche Muster und erzeugt so

^(?!.*-store\.ts$)(?!.*/ai/).*\.ts$

Gelesen heißt das: Treffe jeden Pfad, der auf .ts endet, sofern er weder irgendwo -store.ts am Ende noch /ai/ enthält. Eine Datei wie flight-search.ts passt also, flight-search-store.ts und alles unter /ai/ fallen heraus.

toDependency und formatDependency bereiten die Treffer für eine gut lesbare Ausgabe auf. toDependency extrahiert aus einem tsarch-Verstoß die Quell- und Zieldatei, formatDependency formatiert sie als source -> target:

export function toDependency(violation: unknown): Dependency {
  const { dependency } = violation as {
    dependency: { sourceLabel: string; targetLabel: string };
  };
  return { source: dependency.sourceLabel, target: dependency.targetLabel };
}

export function formatDependency(dependency: Dependency): string {
  return `${dependency.source} -> ${dependency.target}`;
}

Genau diese präzise Ausgabe ist später entscheidend: Sie ist nicht nur Feedback für Entwicklerinnen und Entwickler, sondern auch ein deterministisches Signal, mit dem der Coding-Agent einen Verstoß gezielt beheben kann.

Modern Angular

Mehr zu Signal Forms und moderner Angular-Architektur findest du in meinem neuen eBook Modern Angular. Es behandelt Signals, Architektur, Testing, KI-Assistenten und praxistaugliche Lösungen für moderne Business-Anwendungen.

Modern Angular - Signal-first, Architecture-first, Practice-first

Mehr zum Buch →

Zugriff auf Stores beschränken

Nur Smart Components – und erneut der ai-Layer – bekommen Zugriff auf Stores. Allerdings gibt es hier die bereits erwähnte Ausnahme: Dumb Components dürfen auf Stores im selben Ordner oder in Kindordnern zugreifen. In diesem Fall gehen wir von lokalem State Management aus, das ein Implementierungsdetail der Dumb Component ist. Auch Coordinatoren nehmen wir aus, denn ihre Aufgabe ist es ja gerade, mehrere Stores zu kombinieren.

Das zugehörige it setzt diese Mischung aus genereller Regel und lokaler Ausnahme um:

it('only smart components may access a store (locality and ai excepted)', async () => {
  // Coordinators are a dedicated service layer that may combine several stores.
  const rule = filesOfProject(TS_CONFIG)
    .matchingPattern(anyFileExcept(SMART, AI_LAYER, STORE, COORDINATOR))
    .shouldNot()
    .dependOnFiles()
    .matchingPattern(STORE);

  // Exception: when the store is co-located (same or child folder)
  const violations = (await rule.check())
    .map(toDependency)
    .filter(({ source, target }) => !isLocalAccess(source, target));

  expect(violations.map(formatDependency)).toEqual([]);
});

Die Grundregel selektiert alle Dateien außer Smart Components, ai-Layer, Stores und Coordinatoren und verbietet ihnen den Zugriff auf Stores. Die lokale Ausnahme lässt sich nicht allein über ein Muster ausdrücken – sie hängt von der relativen Lage zweier Dateien ab. Deshalb filtern wir die gefundenen Verstöße nachträglich und lassen alle durch, bei denen der Store lokal zur zugreifenden Datei liegt.

Diese Prüfung übernimmt die Hilfsfunktion isLocalAccess. Sie liefert true, wenn die Zieldatei im selben Ordner wie die Quelle oder in einem Kindordner davon liegt:

export function isLocalAccess(source: string, target: string): boolean {
  const sourceFolder = posix.dirname(source);
  const targetFolder = posix.dirname(target);
  return (
    targetFolder === sourceFolder || targetFolder.startsWith(`${sourceFolder}/`)
  );
}

So bleibt Feature-lokales State Management ein erlaubtes Implementierungsdetail, während der domänen- oder featureübergreifende Zugriff auf fremde Stores weiterhin den Smart Components vorbehalten ist.

Stores dürfen nicht aufeinander zugreifen

Das ist möglicherweise eine etwas kontroversere Regel, aber ich habe mich in meinem Setup entschieden, den Zugriff von Store zu Store nicht zu erlauben – vor allem, um Zyklen zu vermeiden. Benötigt ein Use Case Zugriff auf mehrere Stores, kann er den oben beschriebenen Coordinator verwenden oder auf Eventing zwischen den Stores setzen. Letzteres führt zu einer loseren Kopplung, ersteres ist einfacher.

Das dritte it formuliert diese Regel direkt und ohne Ausnahmen:

it('stores must not access other stores', async () => {
  // Combining several stores is the job of a coordinator, not of a store.
  const rule = filesOfProject(TS_CONFIG)
    .matchingPattern(STORE)
    .shouldNot()
    .dependOnFiles()
    .matchingPattern(STORE);

  const violations = await rule.check();
  expect(violations.map(toDependency).map(formatDependency)).toEqual([]);
});

Jede Datei, die auf -store.ts endet, darf von keiner anderen -store.ts-Datei abhängen. Braucht ein Store doch Zustand aus einem anderen, ist das ein klares Signal, einen Coordinator einzuführen – und genau dorthin lenkt uns diese Regel.

Dumb Components dürfen nicht auf Smart Components zugreifen

Dumb Components sind wiederverwendbare, präsentationsorientierte Bausteine. Sie sollen nichts über die Use Cases wissen, in denen sie verwendet werden – und schon gar nicht auf Smart Components zugreifen, die diese Use Cases orchestrieren. Würden sie es tun, wäre ihre Wiederverwendbarkeit dahin und es entstünden schnell Zyklen.

Das vierte it setzt diese Vorgabe um:

it('dumb components must not access smart components', async () => {
  const rule = filesOfProject(TS_CONFIG)
    .matchingPattern(DUMB)
    .shouldNot()
    .dependOnFiles()
    .matchingPattern(SMART);

  const violations = await rule.check();
  expect(violations.map(toDependency).map(formatDependency)).toEqual([]);
});

Das DUMB-Muster trifft sowohl die Suffixe -card und -pane als auch alles, was in einem ui-Ordner liegt; das SMART-Muster trifft die Use-Case-Suffixe -page, -search, -edit, -detail und -overview. Die Regel hält damit die Flussrichtung sauber: Smart Components dürfen Dumb Components nutzen, aber nicht umgekehrt.

tsarch mit Coding Agent verbinden

Wissen allein reicht nicht – wie schon Sheriff im ersten Teil binden wir auch tsarch als deterministisches Sicherheitsnetz in einen Stop-Hook ein. Dazu nehmen wir die Architektur-Tests einfach in die gemeinsamen Quality-Checks auf, die der Hook bei jeder Runde ausführt. Diese Checks leben im bereits aus dem ersten Teil bekannten Skript scripts/ci-checks.mjs, das die schnellen von den teureren Schritten trennt. tsarch reiht sich dabei in die schnellen Checks ein, gleich neben Lint (inklusive Sheriff):

[...]

const fastSteps = [
  'npx ng lint flights',
  'npm run test:arch'
];

[...]

Damit läuft npm run test:arch – und mit ihm sämtliche tsarch-Regeln – als fester Bestandteil derselben Loop, die schon Lint (inklusive Sheriff) trägt. Die teureren Schritte wie Browser-Tests und Build bleiben den vollständigen Durchläufen vorbehalten, während der Stop-Hook bewusst nur die schnellen Checks ausführt. Schlägt eine Architektur-Regel an, geht der Agent in eine neue Runde und bekommt die konkrete source -> target-Meldung als Input, mit der er sich selbst korrigiert.

Die Registrierung des Hooks für Claude Code erfolgt wie gehabt über .claude/settings.json:

{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "node scripts/hooks/claude-stop-hook.mjs",
            "timeout": 600
          }
        ]
      }
    ]
  }
}

Wie man dasselbe Setup auch für Cursor AI, Codex, Googles Antigravity und andere Coding-Agents bereitstellt – inklusive einer gemeinsamen Quelle der Wahrheit und kleiner Sync-Skripte – beschreibt der erste Teil dieser Reihe ausführlich. Die tsarch-Regeln reihen sich nahtlos in dieses Modell ein, weil sie über dieselben ci-checks aufgerufen werden.

Regeln und Architektur-Dokumente nachziehen

So wertvoll der Stop-Hook ist – am besten ist der Verstoß, der gar nicht erst entsteht. Deshalb gehören dieselben Regeln nicht nur in den tsarch-Test, sondern auch in die Architektur-Dokumente, die von den Rules der jeweiligen Coding-Agents aufgegriffen werden. In docs/architecture-boundaries.md haben wir entsprechend festgehalten, dass nur Stores (und der ai-Layer) direkt auf Data-Access-Services zugreifen dürfen, und in docs/architecture-state-management.md, dass Stores nicht voneinander abhängen, sondern ein Coordinator das Kombinieren übernimmt.

Sind diese Vorgaben sauber dokumentiert und in den Rules als architektur-relevant markiert, liest der Agent sie schon vor einer Änderung und vermeidet den Verstoß proaktiv. Im Idealfall braucht es den Stop-Hook dann gar nicht – er fungiert als letztes Auffangnetz, falls der Agent eine Vorgabe doch einmal übersieht.

Test Drive: Stop-Hook gegen einen riskanten Rename-Prompt

Wie gut dieses Zusammenspiel funktioniert, zeigt ein bewusst heikler Prompt:

Rename the SummaryCoordinator to SummaryStore.

Diese Umbenennung würde aus einem Coordinator dem Namen nach einen Store machen – und damit gegen unsere Regeln verstoßen, denn ein „Store" mit -store.ts dürfte plötzlich nicht mehr mehrere Stores kombinieren. Waren die Einschränkungen in den Docs entsprechend formuliert und wiesen die Rules darauf hin, dass die Docs architektur-relevante Informationen zu Suffixen enthalten, haben sich die getesteten Coding-Agents entschieden, die Docs zu lesen, und den Benutzer über eine mögliche bevorstehende Architekturverletzung informiert:

Der Coding-Agent liest vor der Umbenennung die Architektur-Dokumente, erkennt den drohenden Konventionsbruch und weist den Benutzer darauf hin, bevor er etwas ändert.

Haben wir diese Rules und Informationen hingegen entschärft – in manchen Fällen reichte es sogar, in den Rules die Wichtigkeit der Suffixe nicht zu erwähnen, sodass die Architektur-Docs vom Agenten für ein simples Rename gar nicht als relevant empfunden wurden –, kam er der Aufforderung nach und nannte den Coordinator um. Der Architekturbruch wurde dann aber vom Stop-Hook erkannt: tsarch schlug an, der Coding-Agent informierte daraufhin den Benutzer und bot an, die Aktion wieder rückgängig zu machen:

Nach der Umbenennung schlägt der tsarch-Test im Stop-Hook an; der Coding-Agent meldet den erkannten Architekturbruch und bietet an, die Änderung zurückzunehmen.

Beide Wege führen also zum Ziel – nur an unterschiedlichen Stellen. Sind die Docs und Rules scharf gestellt, greift der Schutz proaktiv; ist eine Lücke geblieben, fängt der Stop-Hook den Verstoß deterministisch ab.

Ergänzung: Verify-and-Fix Skill

Der Stop-Hook läuft automatisch nach jeder Runde des Agenten und beschränkt sich aus Performance-Gründen auf die schnellen Checks. Für eine bewusst angestoßene, vollständige Prüfung – etwa vor dem Commit oder Push – bietet sich ergänzend ein eigener Skill an. Im verlinkten Projekt liegt er unter .agents/skills/verify-and-fix/SKILL.md:

---
name: verify-and-fix
description: [...]
---

# Full Verify and Fix

Run the full quality checks and resolve every problem until they pass. The stop
hook only runs the fast checks, so this skill is the on-demand full pass before
committing or pushing.

## Run

npm run verify

It stops at the first failing step.

[...]

Der Skill startet über npm run verify die vollständige Suite – inklusive der tsarch-Architektur-Tests – und arbeitet sie in einer Propose-and-Confirm-Schleife ab: Bei einem Fehler untersucht der Agent die Ursache, schlägt einen konkreten Fix vor und wartet auf die ausdrückliche Bestätigung des Benutzers, bevor er etwas ändert. Lint, Sheriff und die Architektur-Regeln dürfen dabei niemals aufgeweicht werden, nur damit ein Check grün wird – behoben wird ausschließlich der Code.

Test Drive: verify-and-fix gegen einen manuellen Architekturbruch

Auch diesen Skill schauen wir uns an einem konkreten Fall an. Diesmal benennen wir den Coordinator manuell in einen Store um – also genau die Architekturverletzung, die wir gerade vermeiden wollen – und starten anschließend den Skill über einen einfachen Prompt:

/verify-and-fix

Der Skill führt die vollständigen Checks aus, tsarch meldet den Verstoß, und der Agent präsentiert seinen Befund samt Ursache und Lösungsvorschlag:

Mit aktivierter Propose-and-Confirm-Vorgabe ändert der Skill nichts eigenmächtig, sondern legt dem Benutzer den erkannten Verstoß und einen konkreten Fix-Vorschlag zur Bestätigung vor.

Trade-offs und Grenzen

So nützlich konventionsbasierte Regeln sind – sie haben Grenzen, die man kennen sollte:

  • Suffixe sind keine Wahrheit, sondern eine Konvention. Sie beschreiben die Absicht eines Bausteins, erzwingen sie aber nicht.
  • Falsche Benennung erzeugt falsche Klassifikation. Wer eine Datei – bewusst oder versehentlich – falsch benennt, entzieht sie der passenden Regel oder unterwirft sie einer falschen.
  • Barrel-Files können Regeln verschleiern, wenn Abhängigkeiten nur über gebündelte Re-Exports laufen und nicht sauber bis zur eigentlichen Quelle aufgelöst werden.

Es gibt also Wege, das Ganze auszuhebeln. tsarch ist als eine von mehreren Verteidigungslinien zu verstehen – neben Sheriff, den Architektur-Dokumenten und dem menschlichen Review – und kein Freibrief, generierten Code blind einzuchecken. Der Wert liegt darin, dass die häufigsten und mechanischsten Verstöße deterministisch auffallen, nicht darin, jede denkbare Umgehung zu verhindern.

Mehr dazu: Angular Architecture Workshop: AI & Signals (Remote, Interaktiv, Advanced)

Das Zusammenspiel von AI und Architektur – also genau das, worum es in dieser Serie geht – ist eines der großen Themen unseres Workshops. Werde zum Experten für unternehmensweite und langlebige Angular-Anwendungen und lerne, AI für wartbare Architekturen einzusetzen, statt sie über die Zeit aufzuweichen.

Angular Architecture Workshop

Deutsche Version | English Version

Fazit

Nicht alle technischen Einschränkungen lassen sich über Layer lösen – schon gar nicht bei Feature Slicing, wo Bausteine wie Stores, Clients und Dumb Components feature-lokal beieinanderliegen. tsarch schließt diese Lücke, indem es Architekturregeln an Namenskonventionen knüpft, sie als Unit-Test prüft und über denselben Stop-Hook wie Sheriff in die Feedback-Loop der AI einspielt.

Da dieselben Regeln auch Teil der Architektur-Dokumente sind, greift der Schutz im Idealfall schon proaktiv und der Stop-Hook bleibt das letzte Auffangnetz. Wer eine bewusst angestoßene Vollprüfung bevorzugt, aktiviert dieselben Checks alternativ über einen Skill – wahlweise automatisch behebend oder im Propose-and-Confirm-Modus.

FAQ

Was ist tsarch?
tsarch orientiert sich an der Idee von der bekannten Bibliothek ArchUnit und überträgt sie auf TypeScript-/JavaScript-Projekte. Es parst das Projekt über die Compiler-API und prüft Abhängigkeiten zwischen Dateien in lesbaren, fließend formulierten Regeln, die als Unit-Tests laufen.

Wann reicht Sheriff nicht und man braucht tsarch?
Sheriff lenkt die Kommunikation zwischen Modulen, z.B. entlang von Domänen und Layern. Sobald Feature Slicing erlaubt, dass ein Feature seine eigenen Dumb Components, Stores und Clients mitbringt, reicht das nicht mehr. tsarch knüpft die Regeln dann an Building Blocks, die wir über Datei-Suffixe wie -store.ts, -client.ts oder -coordinator.ts erkennbar machen.

Wie bindet man tsarch in die Feedback-Loop eines Coding-Agents ein?
Die Regeln laufen als ganz normaler Vitest-Test und werden über einen Stop-Hook eingebunden – ein Skript, das der Coding-Agent automatisch am Ende jeder Runde ausführt. Schlägt eine Regel an, bekommt der Agent die konkrete Fehlermeldung als deterministisches Feedback und korrigiert sich selbst.

Worin unterscheiden sich Stop-Hook und verify-and-fix-Skill?
Der Stop-Hook läuft automatisch nach jeder Runde und beschränkt sich aus Performance-Gründen auf die schnellen Checks. Der verify-and-fix-Skill wird vom Benutzer bei Bedarf über einen Prompt angestoßen.

Modern Angular

Architecture · Concepts · Implementation

Ein praktischer Leitfaden für skalierbare Angular-Anwendungen mit modernen Architekturmustern und Signals.

Mehr zum Buch