Layout Convention Contracts¶
Layout convention contracts select source files by folder segment, namespace segment, and file-name prefix/suffix, then validate the declared types in each matched file against type-kind, naming, file/type-name-matching, and matching-interface-counterpart expectations. This governs layout conventions — Services folders containing concrete service classes, Interfaces folders containing only interfaces, a file's primary type matching its file name — not dependency direction, which the dependency, allow-only, and layer order families already cover.
Groups:
strict_layout_conventionsaudit_layout_conventionsstrict_layout_convention_applicabilityaudit_layout_convention_applicability
Example¶
contracts:
strict_layout_conventions:
- id: services-are-concrete-classes
name: services-folder-must-contain-concrete-services
files_matching:
folder_segment: Services
require_type_kind: class
required_name_suffix: Service
require_matching_interface:
name_prefix: I
reason: Every service class needs a matching interface, and the Services folder must not accumulate misplaced interfaces.
audit_layout_conventions:
- id: interfaces-are-interfaces
name: interfaces-folder-must-not-contain-concrete-types
files_matching:
folder_segment: Interfaces
forbid_type_kind: class
When to use¶
Use layout convention contracts when a rule is about where a file lives and what it declares, derived from deterministic source-file and declared-type facts, not from a dependency edge:
- Application Services files must contain concrete service types;
- Application Interfaces files must contain service interfaces, not implementations;
- interface namespaces/folders must not contain concrete implementation classes;
- services namespaces/folders must not contain interfaces;
- a service class should have a matching
I-prefixed interface somewhere in the codebase; - a file's primary declared type should match its file name.
Semantics¶
Selecting files¶
files_matching selects candidate source files using only these fields, all optional, combined with AND semantics (an unset/empty field is not applied):
folder_segment— the file's path (relative to the repository root) must contain this exact folder segment.namespace_segment— the declared type's namespace must contain this exact dot-separated segment.file_name_suffix/file_name_prefix— match the file name (without extension), ordinal.
There is no regex or expression-language selector on these fields. At least one must be populated, or policy loading fails — an unselective matcher would otherwise match every source file.
Folder-segment and file-name selector fields require deterministic source-file data (see Source facts and unavailable data below); a declared type with no resolved source file can never match those fields. namespace_segment still works from reflection-derived namespace facts even without source enrichment.
An optional CEL refinement¶
files_matching.when is an optional ArchLinter CEL Profile v1 boolean predicate that further narrows which declared types in an already-selected file are checked, evaluated against the same closed subject context every selector-backed when uses (subject.role, subject.kind, subject.sourcePaths, subject.sourceDirectoryPrefixes, etc.):
files_matching:
folder_segment: Services
when: subject.role == 'ApplicationService'
when is compiled at policy-load time and fails the load on any compile diagnostic, exactly like every other approved when location in this tool.
Expectations¶
All optional; declare at least one, or policy loading fails as a configuration error:
require_type_kind/forbid_type_kind— the matched file's declared types must (or must not) include a type of this kind (class,interface,struct,enum,record,delegate).required_name_suffix/required_name_prefix/forbidden_name_suffix/forbidden_name_prefix— check each matched declared type's simple name, same semantics as type placement.require_type_name_matches_file_name— the matched file must declare at least one type whose simple name equals the file name (without extension).require_matching_interface— every matched concrete class must have a corresponding interface (name_prefix+ class name, default prefixI) declared somewhere in the analyzed source. Ambiguous candidates (more than one interface with the expected name) are reported as unresolved rather than picked implicitly.max_declarations_per_type— the source declaration inventory for each selected type must not exceed this positive integer. The diagnostic includes the type, observed count, configured maximum, and every declaration path.
Declaration-count expectations use the path-complete source declaration inventory, not the unique
source-file fact used by file-name checks. This preserves the existing ambiguity result for
consumers that require one source path while still making a split type observable to layout policy.
The repository's strict self-policy applies this expectation only to production files under src.
Test fixtures, generated declarations, and language/interop samples that intentionally model
partial-type semantics remain outside that production selector.
Folder purity with all_declarations¶
all_declarations validates every source declaration in selected folders:
all_declarations:
allowed_type_kinds: [interface, class]
allowed_roles: [Exception]
require_abstract_classes: true
Declare at least one of allowed_type_kinds or allowed_roles; when both are present, each
declaration must satisfy both. Roles come from semantic classification. With
require_abstract_classes: true, permitted classes must be explicitly abstract; interfaces
remain valid, while records, structs, delegates, and enums are never reinterpreted as classes.
This is suited to rules such as “Abstractions contains only interfaces or abstract classes” and
“Exceptions contains only classified exception declarations”.
Source facts and unavailable data¶
Layout convention contracts read from the same deterministic source-file and declared-type fact index as type placement's namespace resolution. If a contract is declared but the run has no source-enriched declared-type facts at all (for example, analysis.source_roots is not configured), validation emits one explicit diagnostic explaining that path-based layout checks are unavailable for this run, instead of silently reporting zero violations.
exclude_files_matching reuses the same matcher shape as files_matching and subtracts matched candidates before expectations run. Its own optional when predicates are compiled and evaluated the same way as files_matching.when; empty exclusion matchers are rejected at policy load time.
The effective selector scope is observable in validation reports and explain: the included files_matching matcher and each exclusion are recorded independently as matched, stale, or evaluation-failed when source facts are unavailable.
Applicability inventory¶
Ordinary layout-convention selectors stay backward compatible: a selector that matches zero files is not an error unless an applicability inventory explicitly opts into checking it. An inventory is bounded by one scope under analysis.source_roots; its expected_folders paths are relative to that scope and each references an id from a layout convention in the same strict or audit mode.
analysis:
source_roots: [src]
contracts:
strict_layout_conventions:
- id: services-are-concrete-classes
name: services-folder-must-contain-concrete-services
files_matching:
folder_segment: Services
require_type_kind: class
strict_layout_convention_applicability:
- id: source-layout-folders
name: source-layout-folders-remain-observable
scope: src
exhaustive: true
expected_folders:
- id: services
path: Services
convention_id: services-are-concrete-classes
The inventory produces normal applicability evidence, so existing CLI, JSON, SARIF, testing-adapter, and baseline views report the parent inventory ID, the stable expected-folder control ID, and reason codes. A declared folder with no observable source facts is stale_declaration; a declared folder whose linked selector matches none of its source facts is unexpected_empty_input. With exhaustive: true, every source subject in scope must map to exactly one distinct linked convention: missing mappings are unmapped_subject, and mappings to more than one convention are ambiguous_subject. The inventory retains one scope control for completion state, while each unmapped or ambiguous subject also emits its own path-and-type identity, so an accepted debt for one subject cannot suppress a later one.
audit_layout_convention_applicability uses the same data model but only contributes audit-mode findings. It never changes a strict-mode result unless the corresponding strict inventory is configured.
Violations and ignored violations¶
Diagnostics identify the matched file, the contract, and whichever of expected/actual type kind, expected/actual name, file/type-name mismatch, or expected/actual counterpart applied. ignored_violations entries use the same source_type/forbidden_reference/reason shape as other contract families.
Scope: what's not covered here¶
- No regex or expression-language selectors beyond the bounded
whenrefinement described above. - No runtime dependency-injection resolution.
- No global filesystem or repository-folder discovery. Applicability inventories inspect only declared source roots, their explicit scope, and the source facts already available to the analysis.
- No configurable counterpart naming beyond a single prefix —
require_matching_interfacesupportsname_prefixonly.
Worked, tested examples¶
The Services/Interfaces matching-counterpart shape above, and a files_matching.when
CEL predicate narrowing which declared types are checked, are exercised
end-to-end (pass, fail, and JSON diagnostic output) in
tests/ArchLinterNet.Cli.Tests/LayoutConventionCliTests.cs. The modular-monolith
sample (samples/policies/imports/modular-monolith/architecture/policy/shared/application-layout.arch.yml)
and the Unity-client sample (samples/policies/imports/unity-client/architecture/policy/runtime.arch.yml)
demonstrate the same shape in narrative, composition-focused form.