Layers, Namespace Patterns, and Semantic Selectors¶
Layers are named architecture surfaces used by contracts.
layers:
application:
namespace: MyApp.Application
domain:
namespace: MyApp.Domain
Contract rules reference layer keys such as application and domain.
Semantic selectors¶
Layers may select classified types by exact role and metadata. A selector-only
layer is valid, and a layer that declares both namespace and selector uses
both constraints (logical AND):
layers:
sales_domain:
selector:
role: DomainLayer
metadata:
domain: Sales
domain:
namespace: MyApp.Domain
selector:
role: DomainLayer
Metadata keys and values are matched exactly; multiple metadata entries must all
match. Selectors do not support wildcard or regular-expression values. A valid
selector that matches no loaded types is reported as an empty selector unless
the layer is marked external: true.
Selector when predicates¶
layers.<name>.selector accepts an optional when field: a CEL predicate
that refines the selector's literal role/metadata match. when is
additive — a type belongs to the layer only if it already matches role and
metadata, and when evaluates to true:
layers:
sales_domain:
selector:
role: Domain
when: subject.metadataText["domain"] == "Sales"
when compiles against a fixed subject context exposing identity facts
(fullName, simpleName, namespace, assemblyName, projectName),
classification facts (role, metadataText: Map[String],
metadataBool: Map[Bool]), type facts (kind, isAbstract, isSealed,
baseTypeNames, interfaceTypeNames, attributeTypeNames), and path facts
(sourcePaths, sourceDirectoryPrefixes). Numeric metadata is not exposed to
when — match it with a literal metadata constraint instead.
Important boundary:
- ordinary selector fields such as
role,metadata,namespace, andnamespace_suffixare always literal values, never implicitly parsed as expressions — only the explicitwhenfield carries a CEL predicate; - a selector with no
whenbehaves exactly as before this field existed —whennever runs unless declared; whenevaluating tofalseis an ordinary non-match; awhenevaluation failure (e.g. an unguarded reference to an absentmetadataTextkey) fails the run as a policy/configuration error, not a silent non-match, and is never suppressed by baseline;- a selector whose combined literal-and-
whenmatch set is empty is reported as an empty/stale selector the same way a purely literal empty selector is.
Literal namespace prefixes¶
A literal namespace value matches the exact namespace and child namespaces:
layers:
domain:
namespace: MyApp.Domain
This matches MyApp.Domain, MyApp.Domain.Models, and deeper descendants. It does not match unrelated prefixes such as MyApp.DomainLegacy.
Constrained namespace globs¶
namespace supports a constrained * wildcard when it occupies a complete namespace segment:
layers:
feature_modules:
namespace: MyApp.Features.*
Rules:
*matches exactly one namespace segment.- Descendants under the resolved prefix also match.
*must be a full segment.- Multi-segment wildcards, partial-segment wildcards, character classes, and regular-expression syntax are not supported.
- Leading wildcard patterns are not supported.
Examples:
| Pattern | Namespace | Matches? |
|---|---|---|
MyApp.Features.* |
MyApp.Features.Audio |
yes |
MyApp.Features.* |
MyApp.Features.Audio.Player |
yes |
MyApp.Features.* |
MyApp.Features |
no |
MyApp.Features.* |
MyApp.Other.Audio |
no |
Namespace-allowance contract fields use the same grammar¶
allowed_only_in_namespaces (composition, attribute-usage, interface-implementation
contracts), forbidden_in_namespaces (attribute-usage, interface-implementation
contracts), and must_reside_in_namespaces (type-placement contracts) accept the
exact same constrained glob grammar as namespace above — a literal namespace, or
a pattern with * as one or more complete segments:
contracts:
strict_composition:
- name: container-confined-to-module-bootstrap
allowed_only_in_namespaces: [Product.Modules.*.Composition]
forbidden_apis: [Resolve, Register]
reason: Container resolution must happen only in each module's composition root.
This matches Product.Modules.Orders.Composition, Product.Modules.Billing.Composition,
and their descendants, but not Product.Modules.Composition (zero segments for *) or
Product.Modules.Orders.Sub.Composition (an extra segment before Composition).
There is no namespace_suffix equivalent for these fields — the suffix, if any, is
just part of the same pattern string (as in the Composition example above), not a
separate field.
An entry using unsupported syntax (**, ?, [...], a partial-segment wildcard
such as Foo*, or a bare/leading *) fails policy load with the same actionable
error namespace's glob grammar produces, naming the contract, the field, the
pattern, and the violated rule — it never silently compiles into a pattern that
then matches nothing at scan time.
Namespace suffix¶
Use namespace_suffix to model conventions such as Contracts, Models, or Api slices:
layers:
feature_contracts:
namespace: MyApp.Features.*
namespace_suffix: Contracts
With glob patterns, the suffix is position-fixed immediately after the resolved wildcard segment.
Matches:
MyApp.Features.Audio.ContractsMyApp.Features.Audio.Contracts.Dto
Does not match:
MyApp.Features.Audio.Internal.Contracts
Excluding namespaces from a layer¶
A layer may declare exclude: a list of namespace/namespace_suffix entries
subtracted from the layer's matched scope. A namespace belongs to the layer
only if it matches namespace/namespace_suffix and matches none of the
exclude entries — result = include - union(excludes).
layers:
modules_core:
namespace: Product.Modules.*
exclude:
- namespace: Product.Modules.*.Infrastructure
- namespace: Product.Modules.*.Persistence
This matches every namespace under Product.Modules.* except
Product.Modules.<Module>.Infrastructure and Product.Modules.<Module>.Persistence
(and their descendants). Every contract family that references a layer by
name — dependency, allow-only, external-dependency, protected, cycle, and
acyclic-sibling contracts — observes the narrowed scope automatically, with
no exclusion configuration of its own.
exclude entries use exactly the same namespace glob grammar as namespace
and namespace_suffix above (whole-segment * wildcards, no **/?/character
classes). A layer with no exclude key is unaffected — this is a purely
additive capability with no migration required for existing policies.
Exclusions narrow legitimate scope. Reach for exclude to express "this
is genuinely out of the rule's intended scope," not to silence a rule against
code that is actually in violation. Known debt that should eventually be
fixed belongs in an exact violation baseline,
not a layer exclusion — a baseline records what's wrong and tracks it toward
zero; an exclusion declares the code was never in scope to begin with.
An exclude entry that matches no namespace within its layer's included
scope — most often a typo, such as Product.Modules.*.Persistnce instead of
Product.Modules.*.Persistence — is reported as an unmatched-layer-exclusion policy-consistency finding
(governed by analysis.policy_consistency), so a silently inert exclusion is
visible instead of hiding a mistake.
Overlapping layers¶
Two internal (non-external) layers matching the same concrete type are
reported as a layer-overlap policy-consistency finding, unless one is a
namespace-prefix container of the other (e.g. a coarse core layer and a
nested core_model sub-layer — an intentional hierarchy, not a
contradiction).
If two layers legitimately need to overlap for a reason other than
containment — for example a broad, cross-cutting selector-based layer that is
expected to match types already claimed by a narrower namespace-based layer —
declare it explicitly with overlaps_with:
layers:
sales_domain:
namespace: Product.Modules.Sales.Domain
audited_types:
selector:
role: AuditedEntity
overlaps_with: [sales_domain]
overlaps_with names the other layer(s) this layer is intentionally allowed
to overlap with. Declaring the pairing on either layer is enough to reconcile
it — audited_types above does not also need to appear in
sales_domain.overlaps_with. Each entry must reference a declared layer
name and must not be the layer's own name, or the policy fails to load.
This is a local, reviewable alternative to setting analysis.policy_consistency
away from its error default, which applies to every policy-consistency check
at once, not just this one overlap: warn keeps all of them — duplicate-ID,
allow/forbid-conflict, independence-conflict, protected-importer-conflict,
layer-overlap, and unreachable-contract — in the diagnostics output but stops
them from failing validation, while off suppresses their diagnostics
entirely.
External layers¶
When a layer references namespaces whose assemblies may not be present in the scan environment, set external: true:
layers:
unity_engine:
namespace: UnityEngine
external: true
External layers suppress empty-layer configuration diagnostics, but they can still be used in dependency, allow-only, layer, cycle, independence, and protected-surface contracts.
For new vendor/framework leakage rules, prefer external_dependencies and strict_external / audit_external contracts.
Tips¶
- Prefer narrow concrete layers for important rules.
- Use glob layers for repeated sibling layouts where hand-listing every namespace would be brittle.
- Do not mix broad aggregate layers and overlapping child layers in the same ordered layer contract unless that overlap is deliberate and documented.