ADR-0034: A slice groups .NET types or TypeScript and Python modules; (*) names a slice as ArchUnitNET does; segments keeps the first segments
- Status: Accepted (2026-09-24, by the maintainer)
- Date: 2026-09-24
- Derives from: ADR-0009 (the upstream suite is the specification), ADR-0010 (no
match languagein the engine), ADR-0005 (native additions are additive) - Constrains:
crates/rb-rules/src/slices.rs,crates/rb-config/src/capability.rs,crates/rb-config/src/elements.rs(SliceRule), therb-extract-pythonmodule layer - Implemented by: Wave 2 plan, Step 6
- Requirements: FR-RULE-04
Context
Three facts surfaced while porting ArchUnitNET 0.13.4's SlicesTests and reproducing import-linter's own contracts.
(*)is not one segment. Plan 0002 § 2.6 says "(*)captures one segment,(**)captures the remainder".ArchUnitNET'sSliceRuleInitializer.ParserewritesNs.(*)toNs.(**).and keeps the count of(*)only forPlantUmlFileBuilder; the slice identifier is the whole remainder. Its ownMatchingTestasserts seven slices for bothTestAssembly.Slices.(*)and(**), and three for(**)...- What a slice holds depends on the language. Design § Slice rules makes a slice pattern "a namespace pattern for .NET, a dotted module pattern for Python, and a path pattern for TypeScript". A .NET dependency is between types, as
ArchUnitNETslices; a Python or TypeScript dependency is an import between modules. Slicing Python classes by their module sees inheritance and decorators, not imports, so an import cycle between two packages is invisible. - import-linter's
acyclic_siblingssquashes. Under each package, a child and everything below it is one sibling. NoArchUnitNETpattern says "the first segment, the package itself included":(*)and(*)..keep the whole remainder,(**)..drops a child with no segment after it.
Decision
matchingfollowsArchUnitNETexactly: a single-asterisk pattern is rewritten as upstream rewrites it, and a slice is named by everything between prefix and postfix.Ns.(**)..(orsrc/(**)//for a path) names a slice by its first segment. The plan's sentence is superseded by this ADR, not edited.- The capability data in
rb-configgains a per-language slice unit:Typesfor .NET,Modulesfor TypeScript, JavaScript and Python. The engine reads the unit; it does not match on the language. A module member is matched by its path when the pattern's separator is/, and by its dotted name otherwise. The Python extractor records each local module's dotted name in the module's existingnamespacesfield, as a .NET file records its namespaces. - A slice rule may carry
segments: n, a Rulebearing addition: after the pattern names a slice, only its firstnsegments are kept.acyclic_siblingswith ancestorPis one rule per packageQat or belowP:matching: "Q.(*)",segments: 1,should: beFreeOfCycles.
Consequences
- Gate 2's
SlicesTestspass as upstream asserts them. - A Python or TypeScript slice rule reads the import graph, and the import-linter oracle compares both of its repository's contracts. A mutation that closes a cycle under
importlinter.applicationbreaks the contract in import-linter and fires exactly the matching slice rule. rulebearing import import-linter(2F) writesacyclic_siblingsassegmentsslice rules, one per package.schema/config-v1.jsondocumentssegments;ArchUnitNEThas nothing it maps to, so the coverage tab gains no row.
Alternatives considered
- Make
(*)one segment, as the plan says. Rejected: it contradicts upstream's own tests, which ADR-0009 makes the specification. - Slice Python and TypeScript types. Rejected: their dependencies are imports, which the code layer does not hold; a slice rule would pass over an import cycle.
circular: truewithvia. The design names it as a mapping foracyclic_siblings, but a dependency rule finds cycles between modules, not between squashed packages, so a cycle that runs through two children of a package without a module cycle would pass.- A second pattern syntax. Rejected:
segmentsis one integer beside the patternArchUnitNETusers already know, and leavesmatchingidentical to upstream.