Architecture Rule DSL
Purpose
Routa now has a working TypeScript architecture fitness surface, but the rule set is still embedded inside scripts/fitness/check-backend-architecture.ts. That keeps the first ArchUnitTS integration moving, but it blocks three follow-up goals:
- reusing the same rule intent across TypeScript and Rust backends
- generating or editing rules with LLMs without touching executable code
- feeding a stable, structured rule model into fitness, UI, and future graph-based executors
This document defines a small, engine-neutral Architecture Rule DSL for those goals.
Goals
- Keep one machine-readable rule model that TypeScript and Rust can both parse.
- Preserve the current ArchUnitTS boundary and cycle rules without changing their user-visible meaning.
- Separate rule intent from executor code.
- Make the DSL simple enough for LLM generation and review.
- Leave room for future executors such as Rust graph analysis, dep-tree adapters, or repository-wide topology checks.
Non-Goals
- This is not a full architecture language in the first iteration.
- This does not yet execute every rule family on every engine.
- This does not replace
dependency-cruiser,entrix, or the current architecture API response shape. - This does not move UI i18n into the executor. The DSL carries stable ids and optional display metadata only.
Design Principles
- One file should describe one coherent rule model.
- Selectors should be reusable across rules.
- Rule semantics should not depend on one executor implementation.
- Unsupported rules must fail validation explicitly instead of being silently ignored.
- LLMs should be able to emit valid files with low prompt complexity.
File Format
The canonical format is YAML.
Recommended file extension:
*.archdsl.yaml
Schema id:
routa.archdsl/v1
The top-level DSL file field that carries this identifier is:
schema: routa.archdsl/v1
Recommended location:
architecture/rules/
Core Model
Each DSL file contains:
schemamodeldefaultsselectorsrules
model
model identifies the rule pack as a durable unit.
Fields:
id: stable machine idtitle: human-readable labeldescription: short scope summaryowners: optional logical owners such asfitness,backend, orplatform
defaults
Shared filesystem defaults.
Fields:
root: optional root, default.exclude: optional ignore globs
selectors
Selectors name reusable file scopes.
Current selector kind:
files
Current selector fields:
kindlanguage: exact lowercase valuestypescriptorrust(future values may be added in later schema versions)include: glob listexclude: optional glob listdescription: optional short intent note
Example:
selectors:
core_ts:
kind: files
language: typescript
include:
- src/core/**
rules
Rules describe engine-neutral intent.
Shared rule fields (all rules require these):
id: stable machine idtitle: readable label for CLI/debug outputkind: rule kind, currentlydependencyorcyclesuite: suite tag, currentlyboundariesorcyclesseverity:advisory,warning, orerrorrelation: semantics selectorengine_hints(optional): execution hints, for examplearchunittsand/orgraph
Compatibility constraints:
dependencyrequiresfromandtocyclerequiresscoperelationis required for all rule kindsdependencyrequiresrelation: must_not_depend_oncyclerequiresrelation: must_be_acyclic
Dependency Rule
Fields:
from: selector idrelation: currentlymust_not_depend_onto: selector id
Supported combinations:
kind: dependencysuite: boundariesrelation: must_not_depend_on- executor support:
archunitts(typescript selectors only) orgraph(single language selectors)
Example:
- id: ts_backend_core_no_core_to_client
title: src/core must not depend on src/client
kind: dependency
suite: boundaries
severity: advisory
from: core_ts
relation: must_not_depend_on
to: client_ts
engine_hints:
- archunitts
Cycle Rule
Fields:
scope: selector idrelation: currentlymust_be_acyclic
Supported combinations:
kind: cyclesuite: cyclesrelation: must_be_acyclic- executor support:
archunitts(typescript selectors only) orgraph(single language selectors)
Example:
- id: ts_backend_core_no_cycles
title: src/core should be cycle free
kind: cycle
suite: cycles
severity: advisory
scope: core_ts
relation: must_be_acyclic
engine_hints:
- archunitts
Why YAML
YAML is the recommended canonical syntax because it is already a common configuration format in the repository and is supported well on both implementation paths:
- TypeScript:
js-yamlfor parsing andzodfor validation - Rust:
serde_yamlwith typed enums/structs
That keeps the DSL inspectable by humans, easy for LLMs to emit, and stable for future toolchains.
TypeScript Implementation Strategy
Recommended approach:
- Parse YAML with
js-yaml. - Validate the raw document with
zod. - Compile the normalized model into the current ArchUnitTS
ArchitectureRuleDefinition[]. - Keep the current JSON report shape so existing API/UI consumers remain stable.
Why this approach:
- It reuses the existing
scripts/fitness/check-backend-architecture.tsexecution path. - It keeps the first production rollout close to the current working behavior.
- It makes rule execution a pure compilation step from DSL to ArchUnitTS builders.
Current rollout scope:
filesselectorsdependency+must_not_depend_oncycle+must_be_acyclicboundariesandcyclessuites
Rust Implementation Strategy
Recommended approach:
- Add a new
routa-cli fitness arch-dslcommand. - Parse YAML into typed structs with
serde+serde_yaml. - Run semantic validation:
- schema id is supported
- selector ids are unique
- rules reference existing selectors
kind,relation, and selector language combinations are supported
- Execute
graph-backed rules directly and emit a normalized execution plan as text or JSON.
Why this approach:
- It proves the DSL is not coupled to the TypeScript runtime.
- It gives Routa a second parser and validator immediately.
- It creates a clean handoff point for future Rust-backed architecture executors.
Current execution boundary:
- Rust validates and normalizes the DSL.
- Rust executes
graph-backed dependency and cycle rules directly from the CLI. - ArchUnitTS-compatible rules remain owned by the TypeScript fitness path.
- TypeScript
ArchUnitTSrules still execute throughscripts/fitness/check-backend-architecture.ts.
Normalized Semantic Contract
Both implementations should converge on the same semantic assumptions:
- selector ids are globally unique within one file
- rule ids are globally unique within one file
- every rule references existing selectors
dependencyrules requirefromandtocyclerules requirescope- TypeScript ArchUnitTS compilation currently only supports
language: typescript - unsupported combinations must produce explicit validation errors
LLM-Friendly Case Format
The LLM authoring format should be Markdown with YAML frontmatter and predictable sections.
Recommended directory:
architecture/rules/cases/
Recommended file extension:
*.archdsl.md
Required frontmatter:
schemacase_idtarget_dsloutput_formattemperature_hint
Frontmatter semantics:
schemamust berouta.archdsl.case/v1case_idis a stable ID for the case prompt and review tracetarget_dslis the repository-relative output path for the generated DSL fileoutput_formatis the required generated artifact mode, currentlyyamltemperature_hintkeeps generation deterministic and low-variance, currentlylow
Recommended sections:
# Goal## Context## Selector Catalog## Required Rules## Constraints## Output Contract
Unsupported combinations and validation failures:
archunittscan only run withtypescriptselectors and exactly one include pattern per selectorgraphruns only when all referenced selectors use the same language- If a rule references unknown selectors, mixed graph languages, or incompatible engine constraints, validation reports an explicit issue and marks plan/validation as failed
Why this format:
- frontmatter carries stable routing metadata
- headings give LLMs consistent anchors
- the file stays diff-friendly and reviewable
- the output contract can insist on YAML-only emission
Validation Workflow
- Author or update the Markdown case.
- Ask an LLM to emit YAML only for the target DSL.
- Validate the emitted YAML with the TypeScript compiler path.
- Validate the same YAML with the Rust parser path.
- Promote successful output into
architecture/rules/*.archdsl.yaml.
This makes the Markdown case an input contract for generation, not the source of truth for execution.
Current Layout
The current rollout uses these files:
architecture/rules/backend-core.archdsl.yamlarchitecture/rules/cases/backend-core.archdsl.mdscripts/fitness/architecture-rule-dsl.tsscripts/fitness/check-backend-architecture.tscrates/routa-cli/src/commands/fitness/arch_dsl.rs
Future Extensions
Expected next rule families after the first rollout:
- layered rules
- slice isolation rules
- crate or package dependency rules
- forbidden symbol/provider leak rules
- graph-backed selectors and quantums integration
The v1 schema is intentionally small so those extensions can be added without retrofitting the first four backend-core rules.