ArchUnitTS currently offers PlantUML-based architecture checks, but current behavior is primarily file/slice-oriented. That makes it hard to enforce class- and interface-oriented PlantUML diagrams in a way that matches TypeScript source-level intent.
This RFC proposes a new PlantUML enforcement mode for ArchUnitTS that operates at TypeScript symbol level instead of file-path level.
Goal: allow unit tests to verify whether explicit TypeScript class/interface relationships satisfy a PlantUML diagram using deterministic static-analysis semantics.
Problem
Current PlantUML support is useful for simplified slice rules, but it does not map well to diagrams whose nodes are classes/interfaces and whose relationships express source-level semantics such as:
A ..|> B = A implements B
A --|> B = A extends B
A ..> B = A has a class-level static dependency on B
A --> B = A has a class-level direct runtime dependency on B
A::p ..> B = property p on A has a static dependency on B
A::p --> B = property p on A has a direct runtime dependency on B
A::m ..> B = property m on A has a static dependency on B
A::m --> B = property m on A has a direct runtime dependency on B
In AI coded projects, it is useful to test architecture against a diagram written in those terms to enforce correctness.
The main gap is that file-level dependency extraction cannot reliably answer which class or class method depends on which target class/interface or target class/interface method.
Proposal
Add a new PlantUML enforcement mode that:
- Parses PlantUML class/interface relationships.
- Extracts symbol-level relationships from AST + type resolution.
- Compares extracted relationships against diagram relationships.
This mode should replace existing file/slice behavior for UML class diagrams.
Design Principle
PlantUML should be interpreted here as static TypeScript architecture constraints. The checker should only enforce relationships that are explicitly visible in TypeScript source via AST and symbol resolution.
It should not infer:
- hidden DI container wiring
- reflection-only dependencies
- string-key runtime lookups
It should also keep class-level, property-level, and method-level relationships distinct.
Proposed Semantics
A ..|> B
Interpretation: A explicitly implements B.
Pass when:
A has an implements B heritage clause
Do not pass when:
A is only structurally assignable to B
A merely references B as a type
A --|> B
Interpretation: A explicitly extends B.
Pass when:
A has an extends B heritage clause
Do not pass when:
A is assignable to B without explicit inheritance
A ..> B
Interpretation: A has a class-level static dependency on B.
Pass when A references B in class-level type position, including:
- constructor parameter types
- type arguments in generics
Do not pass when:
B appears only in property/field types
B appears only in method parameter types
B appears only in method return types
B appears only in local variable types
B appears only inside method-level generic/type expressions
A --> B
Interpretation: A has a class-level direct runtime dependency on B.
Pass when A explicitly references symbol B in class-level runtime expression position, including:
- direct class-level runtime references owned by class declaration itself
- constructor body expressions referencing
B, e.g. new B(), B.staticMethod(), or this.b.run()
Do not pass when:
B appears only as decorator/helper argument value, e.g. @Generator(B)
B appears only inside property initializers
B appears only inside method bodies
Property and method dependencies should be modeled separately from class dependencies.
A::p ..> B
Interpretation: property p on class A has a property-level static dependency on B.
Pass when p references B in type position, including:
- property/field types
- property-level generic/type expressions
- getter and setter types
A::p --> B
Interpretation: property p on class A has a property-level direct runtime dependency on B.
Pass when p explicitly references runtime symbol B, including:
- field initializers
- property initializers
- getter and setter bodies
A::m ..> B
Interpretation: method m on class A has a method-level static dependency on B.
Pass when m references B in type position, including:
- method parameter types
- method return types
- local variable types
- method-level generic/type expressions
A::m --> B
Interpretation: method m on class A has a method-level direct runtime dependency on B.
Pass when m explicitly references runtime symbol B, including:
new B()
B.staticMethod()
B.staticProperty
- passing
B as runtime value
Do not pass when:
- method only calls through an injected abstraction handle and symbol
B is not directly referenced
Explicit Non-Goals
This proposal does not aim to:
- implement full UML semantics
- infer runtime object graphs
- validate method signatures against diagrams
- infer hidden dependencies from DI frameworks or reflection
- treat structural typing as equivalent to explicit
implements
Comparison Behavior
The checker should support both:
-
Required-edge validation
Fail if the diagram requires a relationship that code does not contain.
-
Forbidden-edge validation
Fail if code contains a relationship not required in the diagram.
Default: Required-edge validation only
Relation Matching
Default comparison should be strict:
..|> satisfies only ..|>
--|> satisfies only --|>
..> satisfies only ..>
--> satisfies only -->
If ArchUnitTS wants a relaxed compatibility mode later, that can be optional. It should not be the default.
Examples
Example 1: Realization
PlantUML:
@startuml
class UserService
interface UserRepository
UserService ..|> UserRepository
@enduml
TypeScript pass:
class UserService implements UserRepository {}
TypeScript fail:
class UserService {
save(): void {}
}
const repo: UserRepository = new UserService();
Reason: structural assignability should not satisfy ..|>.
Example 2: Class-Level Type Dependency
PlantUML:
@startuml
class A
class B
A ..> B
@enduml
TypeScript pass:
class A {
constructor(b: B) {}
}
TypeScript fail:
class A {
public b!: B;
}
Reason: property dependencies are separate from class dependencies.
Example 3: Class-Level Runtime Dependency
PlantUML:
@startuml
class A
class Uses
A --> Uses
@enduml
TypeScript pass:
TypeScript fail:
Reason: property dependencies are separate from class dependencies.
Example 4: Property-Level Type Dependency
Property-level model:
TypeScript pass:
Example 5: Property-Level Runtime Dependency
Property-level model:
TypeScript pass:
Example 6: Method-Level Type Dependency
Method-level model:
TypeScript pass:
class A {
run(x: B): void {}
}
Example 7: Method-Level Runtime Dependency
Method-level model:
TypeScript pass:
class A {
run() {
return new B();
}
}
Implementation Notes
This likely requires:
- TS compiler API symbol resolution
- declaration-level graph extraction
- relation extraction by syntax category
- PlantUML node-to-symbol resolution
Existing file-level graph extraction is not sufficient by itself for this mode.
Acceptance Criteria
An implementation would be successful if it can reliably test:
- explicit
implements for ..|>
- explicit
extends for --|>
- constructor parameter types for class-level
..>
- type arguments in generics for
..>
- class-level runtime symbol refs for
-->
- property/field types for property-level
..>
- property initializers for property-level
-->
- getter/setter signatures for property-level
..>
- getter/setter bodies for property-level
-->
- method parameter/return/local/generic refs for method-level
..>
- direct runtime symbol refs in methods for method-level
-->
- strict failure for structural compatibility without
implements
- strict failure for missing required dependencies
Open Questions
- Should strict failure for missing UML relations be supported?
- Should class diagrams stay limited to class/interface owners only?
- Should namespaces be supported as part of this change?
Request For Feedback
Main feedback requested:
- Is symbol-level PlantUML enforcement in scope for ArchUnitTS?
- Are these explicit TypeScript semantics acceptable?
- Should this be implemented as a new API, or a new mode on existing APIs?
ArchUnitTS currently offers PlantUML-based architecture checks, but current behavior is primarily file/slice-oriented. That makes it hard to enforce class- and interface-oriented PlantUML diagrams in a way that matches TypeScript source-level intent.
This RFC proposes a new PlantUML enforcement mode for ArchUnitTS that operates at TypeScript symbol level instead of file-path level.
Goal: allow unit tests to verify whether explicit TypeScript class/interface relationships satisfy a PlantUML diagram using deterministic static-analysis semantics.
Problem
Current PlantUML support is useful for simplified slice rules, but it does not map well to diagrams whose nodes are classes/interfaces and whose relationships express source-level semantics such as:
A ..|> B=A implements BA --|> B=A extends BA ..> B=Ahas a class-level static dependency onBA --> B=Ahas a class-level direct runtime dependency onBA::p ..> B= propertyponAhas a static dependency onBA::p --> B= propertyponAhas a direct runtime dependency onBA::m ..> B= propertymonAhas a static dependency onBA::m --> B= propertymonAhas a direct runtime dependency onBIn AI coded projects, it is useful to test architecture against a diagram written in those terms to enforce correctness.
The main gap is that file-level dependency extraction cannot reliably answer which class or class method depends on which target class/interface or target class/interface method.
Proposal
Add a new PlantUML enforcement mode that:
This mode should replace existing file/slice behavior for UML class diagrams.
Design Principle
PlantUML should be interpreted here as static TypeScript architecture constraints. The checker should only enforce relationships that are explicitly visible in TypeScript source via AST and symbol resolution.
It should not infer:
It should also keep class-level, property-level, and method-level relationships distinct.
Proposed Semantics
A ..|> BInterpretation:
Aexplicitlyimplements B.Pass when:
Ahas animplements Bheritage clauseDo not pass when:
Ais only structurally assignable toBAmerely referencesBas a typeA --|> BInterpretation:
Aexplicitlyextends B.Pass when:
Ahas anextends Bheritage clauseDo not pass when:
Ais assignable toBwithout explicit inheritanceA ..> BInterpretation:
Ahas a class-level static dependency onB.Pass when
AreferencesBin class-level type position, including:Do not pass when:
Bappears only in property/field typesBappears only in method parameter typesBappears only in method return typesBappears only in local variable typesBappears only inside method-level generic/type expressionsA --> BInterpretation:
Ahas a class-level direct runtime dependency onB.Pass when
Aexplicitly references symbolBin class-level runtime expression position, including:B, e.g.new B(),B.staticMethod(), orthis.b.run()Do not pass when:
Bappears only as decorator/helper argument value, e.g.@Generator(B)Bappears only inside property initializersBappears only inside method bodiesProperty and method dependencies should be modeled separately from class dependencies.
A::p ..> BInterpretation: property
pon classAhas a property-level static dependency onB.Pass when
preferencesBin type position, including:A::p --> BInterpretation: property
pon classAhas a property-level direct runtime dependency onB.Pass when
pexplicitly references runtime symbolB, including:A::m ..> BInterpretation: method
mon classAhas a method-level static dependency onB.Pass when
mreferencesBin type position, including:A::m --> BInterpretation: method
mon classAhas a method-level direct runtime dependency onB.Pass when
mexplicitly references runtime symbolB, including:new B()B.staticMethod()B.staticPropertyBas runtime valueDo not pass when:
Bis not directly referencedExplicit Non-Goals
This proposal does not aim to:
implementsComparison Behavior
The checker should support both:
Required-edge validation
Fail if the diagram requires a relationship that code does not contain.
Forbidden-edge validation
Fail if code contains a relationship not required in the diagram.
Default: Required-edge validation only
Relation Matching
Default comparison should be strict:
..|>satisfies only..|>--|>satisfies only--|>..>satisfies only..>-->satisfies only-->If ArchUnitTS wants a relaxed compatibility mode later, that can be optional. It should not be the default.
Examples
Example 1: Realization
PlantUML:
TypeScript pass:
TypeScript fail:
Reason: structural assignability should not satisfy
..|>.Example 2: Class-Level Type Dependency
PlantUML:
TypeScript pass:
TypeScript fail:
Reason: property dependencies are separate from class dependencies.
Example 3: Class-Level Runtime Dependency
PlantUML:
TypeScript pass:
TypeScript fail:
Reason: property dependencies are separate from class dependencies.
Example 4: Property-Level Type Dependency
Property-level model:
TypeScript pass:
Example 5: Property-Level Runtime Dependency
Property-level model:
TypeScript pass:
Example 6: Method-Level Type Dependency
Method-level model:
TypeScript pass:
Example 7: Method-Level Runtime Dependency
Method-level model:
TypeScript pass:
Implementation Notes
This likely requires:
Existing file-level graph extraction is not sufficient by itself for this mode.
Acceptance Criteria
An implementation would be successful if it can reliably test:
implementsfor..|>extendsfor--|>..>..>-->..>-->..>-->..>-->implementsOpen Questions
Request For Feedback
Main feedback requested: