This is an automated email from the ASF dual-hosted git repository.

ahuber pushed a commit to branch 3051.validate.memberId.clash
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit 51d6f916911c95a58b8882305932a95e9645d722
Author: Andi Huber <[email protected]>
AuthorDate: Wed May 3 09:48:16 2023 +0200

    CAUSEWAY-3051: simplify MM validator type hierarchy
---
 .../progmodel/ProgrammingModelConstants.java       |   2 +
 ...reteTypeToBeIncludedWithMetamodelValidator.java |   7 +-
 .../actions/action/ActionOverloadingValidator.java |   6 +-
 .../annotation/HomePageFacetAnnotationFactory.java |   8 +-
 .../DomainObjectAnnotationFacetFactory.java        |   8 +-
 ...tionEnforcesMetamodelContributionValidator.java |   6 +-
 .../MethodPrefixBasedFacetFactoryAbstract.java     |   6 +-
 ...tProcessor.java => MetaModelPostProcessor.java} |  21 ++-
 ...ct.java => MetaModelPostProcessorAbstract.java} |   6 +-
 ...ssOnActionFromConfiguredRegexPostProcessor.java |   4 +-
 .../all/DescribedAsFromTypePostProcessor.java      |   4 +-
 ...stProcessor.java => SanityChecksValidator.java} |  71 ++++++-
 .../i18n/SynthesizeObjectNamingPostProcessor.java  |   4 +-
 .../all/i18n/TranslationPostProcessor.java         |   4 +-
 .../authorization/AuthorizationPostProcessor.java  |   4 +-
 ...ynthesizeDomainEventsForMixinPostProcessor.java |   4 +-
 ...NavigationFacetFromHiddenTypePostProcessor.java |   4 +-
 .../object/ProjectionFacetsPostProcessor.java      |   4 +-
 .../param/ChoicesAndDefaultsPostProcessor.java     |   4 +-
 .../param/TypicalLengthFromTypePostProcessor.java  |   4 +-
 .../DisabledFromImmutablePostProcessor.java        |   4 +-
 .../core/metamodel/progmodel/ProgrammingModel.java |  16 +-
 .../progmodel/ProgrammingModelAbstract.java        |  14 +-
 .../progmodel/ProgrammingModelInitFilter.java      |   4 +-
 .../ProgrammingModelInitFilterDefault.java         |   4 +-
 .../dflt/ProgrammingModelFacetsJava11.java         |   4 +-
 .../title/TitlesAndTranslationsValidator.java      |  22 +--
 .../specloader/SpecificationLoaderDefault.java     |  28 +--
 .../specloader/{_Util.java => _LogUtil.java}       |   2 +-
 .../core/metamodel/specloader/_ValidateUtil.java   |  88 +++++++++
 .../specloader/postprocessor/PostProcessor.java    |  69 ++-----
 .../specloader/validator/MetaModelValidator.java   |  34 +++-
 .../validator/MetaModelVisitingValidator.java      |   4 +-
 .../MetaModelVisitingValidatorAbstract.java        |  45 -----
 .../facets/TenantedAuthorizationPostProcessor.java |   4 +-
 ...etaModelVisitingValidatorForClauseAbstract.java |   6 +-
 .../DomainModelTest_usingBadDomain.java            |  12 ++
 .../bad/Configuration_usingInvalidDomain.java      |   1 +
 .../testdomain/model/bad/InvalidMemberIdClash.java | 208 +++++++++++++++++++++
 39 files changed, 526 insertions(+), 224 deletions(-)

diff --git 
a/core/config/src/main/java/org/apache/causeway/core/config/progmodel/ProgrammingModelConstants.java
 
b/core/config/src/main/java/org/apache/causeway/core/config/progmodel/ProgrammingModelConstants.java
index 53a7713ff1..dc6413ef21 100644
--- 
a/core/config/src/main/java/org/apache/causeway/core/config/progmodel/ProgrammingModelConstants.java
+++ 
b/core/config/src/main/java/org/apache/causeway/core/config/progmodel/ProgrammingModelConstants.java
@@ -533,6 +533,8 @@ public final class ProgrammingModelConstants {
                 + "Tuple type ${patType} referenced by @ParameterTuple 
annotated parameter has no or more than one public constructor."),
         VETOED_OR_MANAGED_TYPE_NOT_ALLOWED_TO_ENTER_METAMODEL("${type}: has a 
member with either vetoed or managed "
                 + "element-type ${elementType}, which is not allowed"),
+        MEMBER_ID_CLASH("${type}: has members using the same member-id "
+                + "'${memberId}', which is not allowed; 
clashes:\n\t[1]${member1}\n\t[2]${member2}"),
         ;
 
         private final String template;
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/actions/action/ActionAnnotationShouldEnforceConcreteTypeToBeIncludedWithMetamodelValidator.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/actions/action/ActionAnnotationShouldEnforceConcreteTypeToBeIncludedWithMetamodelValidator.java
index 2697ca2081..502ea2829a 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/actions/action/ActionAnnotationShouldEnforceConcreteTypeToBeIncludedWithMetamodelValidator.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/actions/action/ActionAnnotationShouldEnforceConcreteTypeToBeIncludedWithMetamodelValidator.java
@@ -29,14 +29,13 @@ import 
org.apache.causeway.core.metamodel.context.MetaModelContext;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
-import 
org.apache.causeway.core.metamodel.specloader.validator.MetaModelVisitingValidatorAbstract;
+import 
org.apache.causeway.core.metamodel.specloader.validator.MetaModelValidatorAbstract;
 import 
org.apache.causeway.core.metamodel.specloader.validator.ValidationFailure;
 
-import lombok.NonNull;
 import lombok.val;
 
 public class 
ActionAnnotationShouldEnforceConcreteTypeToBeIncludedWithMetamodelValidator
-extends MetaModelVisitingValidatorAbstract {
+extends MetaModelValidatorAbstract {
 
     @Inject
     public 
ActionAnnotationShouldEnforceConcreteTypeToBeIncludedWithMetamodelValidator(final
 MetaModelContext mmc) {
@@ -44,7 +43,7 @@ extends MetaModelVisitingValidatorAbstract {
     }
 
     @Override
-    public void validate(final @NonNull ObjectSpecification spec) {
+    public void validateObjectEnter(final ObjectSpecification spec) {
         if(spec.getBeanSort()==BeanSort.UNKNOWN
                 && !spec.isAbstract()) {
 
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/actions/action/ActionOverloadingValidator.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/actions/action/ActionOverloadingValidator.java
index 409da3cd88..e678d93cb0 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/actions/action/ActionOverloadingValidator.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/actions/action/ActionOverloadingValidator.java
@@ -28,7 +28,7 @@ import 
org.apache.causeway.core.metamodel.context.MetaModelContext;
 import org.apache.causeway.core.metamodel.spec.ActionScope;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
-import 
org.apache.causeway.core.metamodel.specloader.validator.MetaModelVisitingValidatorAbstract;
+import 
org.apache.causeway.core.metamodel.specloader.validator.MetaModelValidatorAbstract;
 import 
org.apache.causeway.core.metamodel.specloader.validator.ValidationFailure;
 
 import lombok.NonNull;
@@ -43,7 +43,7 @@ import lombok.val;
  * @see <a 
href="https://issues.apache.org/jira/browse/CAUSEWAY-2493";>CAUSEWAY-2493</a>
  */
 public class ActionOverloadingValidator
-extends MetaModelVisitingValidatorAbstract {
+extends MetaModelValidatorAbstract {
 
     @Inject
     public ActionOverloadingValidator(final MetaModelContext mmc) {
@@ -51,7 +51,7 @@ extends MetaModelVisitingValidatorAbstract {
     }
 
     @Override
-    public void validate(final @NonNull ObjectSpecification spec) {
+    public void validateObjectEnter(final @NonNull ObjectSpecification spec) {
 
         if(spec.getBeanSort()!=BeanSort.UNKNOWN
                 && !spec.isAbstract()) {
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/actions/homepage/annotation/HomePageFacetAnnotationFactory.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/actions/homepage/annotation/HomePageFacetAnnotationFactory.java
index 50c0942a23..13901dbb23 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/actions/homepage/annotation/HomePageFacetAnnotationFactory.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/actions/homepage/annotation/HomePageFacetAnnotationFactory.java
@@ -40,7 +40,7 @@ import 
org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
 import 
org.apache.causeway.core.metamodel.specloader.validator.MetaModelValidator;
-import 
org.apache.causeway.core.metamodel.specloader.validator.MetaModelVisitingValidatorAbstract;
+import 
org.apache.causeway.core.metamodel.specloader.validator.MetaModelValidatorAbstract;
 import 
org.apache.causeway.core.metamodel.specloader.validator.ValidationFailure;
 
 import static org.apache.causeway.commons.internal.functions._Predicates.not;
@@ -78,12 +78,12 @@ implements MetaModelRefiner {
     }
 
     private MetaModelValidator newValidatorVisitor(final MetaModelContext mmc) 
{
-        return new MetaModelVisitingValidatorAbstract(mmc) {
+        return new MetaModelValidatorAbstract(mmc) {
 
             private final Map<String, ObjectAction> actionsHavingHomePageFacet 
= _Maps.newHashMap();
 
             @Override
-            public void validate(final @NonNull ObjectSpecification spec) {
+            public void validateObjectEnter(final @NonNull ObjectSpecification 
spec) {
                 if(spec.isInjectable()) {
                     return;
                 }
@@ -106,7 +106,7 @@ implements MetaModelRefiner {
             }
 
             @Override
-            public void summarize() {
+            public void validateExit() {
                 if(actionsHavingHomePageFacet.size()>1) {
 
                     final Set<String> homepageActionIdSet = 
actionsHavingHomePageFacet.values().stream()
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/DomainObjectAnnotationFacetFactory.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/DomainObjectAnnotationFacetFactory.java
index b26736980a..f3003b478e 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/DomainObjectAnnotationFacetFactory.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/DomainObjectAnnotationFacetFactory.java
@@ -73,7 +73,7 @@ import 
org.apache.causeway.core.metamodel.facets.object.viewmodel.ViewModelFacet
 import org.apache.causeway.core.metamodel.object.MmEventUtils;
 import org.apache.causeway.core.metamodel.progmodel.ProgrammingModel;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
-import 
org.apache.causeway.core.metamodel.specloader.validator.MetaModelVisitingValidatorAbstract;
+import 
org.apache.causeway.core.metamodel.specloader.validator.MetaModelValidatorAbstract;
 import 
org.apache.causeway.core.metamodel.specloader.validator.ValidationFailure;
 
 import static org.apache.causeway.commons.internal.base._NullSafe.stream;
@@ -562,13 +562,13 @@ implements
 
         programmingModel
         .addValidator(
-            new 
MetaModelVisitingValidatorAbstract(programmingModel.getMetaModelContext()){
+            new 
MetaModelValidatorAbstract(programmingModel.getMetaModelContext()){
 
                 final _Multimaps.ListMultimap<String, ObjectSpecification> 
specsByLogicalTypeName =
                         _Multimaps.newConcurrentListMultimap();
 
                 @Override
-                public void validate(final ObjectSpecification objSpec) {
+                public void validateObjectEnter(final ObjectSpecification 
objSpec) {
 
                     // @DomainObject(logicalTypeName=...) must be unique among 
non-abstract types
                     // Eg. having an ApplicationUser interface and a concrete 
ApplicationUser (JDO)
@@ -590,7 +590,7 @@ implements
                 }
 
                 @Override
-                public void summarize() {
+                public void validateExit() {
 
                     specsByLogicalTypeName.forEach((logicalTypeName, 
collidingSpecs)->{
                         if(isObjectTypeCollision(collidingSpecs)) {
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/methods/DomainIncludeAnnotationEnforcesMetamodelContributionValidator.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/methods/DomainIncludeAnnotationEnforcesMetamodelContributionValidator.java
index 615817439b..226cb60859 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/methods/DomainIncludeAnnotationEnforcesMetamodelContributionValidator.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/methods/DomainIncludeAnnotationEnforcesMetamodelContributionValidator.java
@@ -46,7 +46,7 @@ import 
org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
 import 
org.apache.causeway.core.metamodel.specloader.specimpl.ObjectMemberAbstract;
 import 
org.apache.causeway.core.metamodel.specloader.specimpl.ObjectSpecificationAbstract;
-import 
org.apache.causeway.core.metamodel.specloader.validator.MetaModelVisitingValidatorAbstract;
+import 
org.apache.causeway.core.metamodel.specloader.validator.MetaModelValidatorAbstract;
 import 
org.apache.causeway.core.metamodel.specloader.validator.ValidationFailure;
 
 import lombok.val;
@@ -56,7 +56,7 @@ import lombok.val;
  * @see org.apache.causeway.applib.annotation.Domain.Include
  */
 public class DomainIncludeAnnotationEnforcesMetamodelContributionValidator
-extends MetaModelVisitingValidatorAbstract {
+extends MetaModelValidatorAbstract {
 
     private final _ClassCache classCache;
 
@@ -67,7 +67,7 @@ extends MetaModelVisitingValidatorAbstract {
     }
 
     @Override
-    public void validate(final ObjectSpecification spec) {
+    public void validateObjectEnter(final ObjectSpecification spec) {
 
         if(!(spec instanceof ObjectSpecificationAbstract)
                 || spec.isAbstract()
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/methods/MethodPrefixBasedFacetFactoryAbstract.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/methods/MethodPrefixBasedFacetFactoryAbstract.java
index ddab2f6ea8..57c3b2a87e 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/methods/MethodPrefixBasedFacetFactoryAbstract.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/methods/MethodPrefixBasedFacetFactoryAbstract.java
@@ -29,7 +29,7 @@ import 
org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
 import 
org.apache.causeway.core.metamodel.specloader.specimpl.ObjectMemberAbstract;
 import 
org.apache.causeway.core.metamodel.specloader.specimpl.ObjectSpecificationAbstract;
-import 
org.apache.causeway.core.metamodel.specloader.validator.MetaModelVisitingValidatorAbstract;
+import 
org.apache.causeway.core.metamodel.specloader.validator.MetaModelValidatorAbstract;
 import 
org.apache.causeway.core.metamodel.specloader.validator.ValidationFailure;
 
 import lombok.Getter;
@@ -71,7 +71,7 @@ implements MethodPrefixBasedFacetFactory {
         }
 
         programmingModel
-        .addValidator(new 
MetaModelVisitingValidatorAbstract(programmingModel.getMetaModelContext()) {
+        .addValidator(new 
MetaModelValidatorAbstract(programmingModel.getMetaModelContext()) {
 
             @Override
             public String toString() {
@@ -80,7 +80,7 @@ implements MethodPrefixBasedFacetFactory {
             }
 
             @Override
-            public void validate(final ObjectSpecification spec) {
+            public void validateObjectEnter(final ObjectSpecification spec) {
 
                 if(spec.isInjectable()) {
                     return;
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/ObjectSpecificationPostProcessor.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/MetaModelPostProcessor.java
similarity index 79%
rename from 
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/ObjectSpecificationPostProcessor.java
rename to 
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/MetaModelPostProcessor.java
index 07ee364144..724af6155b 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/ObjectSpecificationPostProcessor.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/MetaModelPostProcessor.java
@@ -25,17 +25,26 @@ import 
org.apache.causeway.core.metamodel.spec.feature.ObjectActionParameter;
 import org.apache.causeway.core.metamodel.spec.feature.OneToManyAssociation;
 import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
 
-public interface ObjectSpecificationPostProcessor
+public interface MetaModelPostProcessor
 extends HasMetaModelContext {
 
     default boolean isEnabled() {
         return true;
     }
 
-    default void postProcessObject(final ObjectSpecification objSpec) {};
-    default void postProcessAction(final ObjectSpecification objSpec, final 
ObjectAction act) {};
-    default void postProcessParameter(final ObjectSpecification objSpec, final 
ObjectAction act, final ObjectActionParameter param) {};
-    default void postProcessProperty(final ObjectSpecification objSpec, final 
OneToOneAssociation prop) {};
-    default void postProcessCollection(final ObjectSpecification objSpec, 
final OneToManyAssociation coll) {};
+    /** entry to post-processing of specified {@code objSpec} */
+    default void postProcessObject(final ObjectSpecification objSpec) {}
+
+    /** post process action - mixed-in included */
+    default void postProcessAction(final ObjectSpecification objSpec, final 
ObjectAction act) {}
+
+    /** post process action-parameter - mixed-in included */
+    default void postProcessParameter(final ObjectSpecification objSpec, final 
ObjectAction act, final ObjectActionParameter param) {}
+
+    /** post process property - mixed-in included */
+    default void postProcessProperty(final ObjectSpecification objSpec, final 
OneToOneAssociation prop) {}
+
+    /** post process collection - mixed-in included */
+    default void postProcessCollection(final ObjectSpecification objSpec, 
final OneToManyAssociation coll) {}
 
 }
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/ObjectSpecificationPostProcessorAbstract.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/MetaModelPostProcessorAbstract.java
similarity index 90%
rename from 
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/ObjectSpecificationPostProcessorAbstract.java
rename to 
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/MetaModelPostProcessorAbstract.java
index 367f7b1d4e..809e54a105 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/ObjectSpecificationPostProcessorAbstract.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/MetaModelPostProcessorAbstract.java
@@ -28,13 +28,13 @@ import lombok.Getter;
 import lombok.NonNull;
 import lombok.val;
 
-public abstract class ObjectSpecificationPostProcessorAbstract
-implements ObjectSpecificationPostProcessor {
+public abstract class MetaModelPostProcessorAbstract
+implements MetaModelPostProcessor {
 
     @Getter(onMethod_ = {@Override})
     private final @NonNull MetaModelContext metaModelContext;
 
-    protected ObjectSpecificationPostProcessorAbstract(final MetaModelContext 
metaModelContext) {
+    protected MetaModelPostProcessorAbstract(final MetaModelContext 
metaModelContext) {
         super();
         this.metaModelContext = metaModelContext;
     }
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/all/CssOnActionFromConfiguredRegexPostProcessor.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/all/CssOnActionFromConfiguredRegexPostProcessor.java
index afb5fde78e..43609da4e7 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/all/CssOnActionFromConfiguredRegexPostProcessor.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/all/CssOnActionFromConfiguredRegexPostProcessor.java
@@ -26,12 +26,12 @@ import 
org.apache.causeway.core.metamodel.facets.members.cssclass.CssClassFacet;
 import 
org.apache.causeway.core.metamodel.facets.members.cssclass.annotprop.CssClassFacetOnActionFromConfiguredRegex;
 import 
org.apache.causeway.core.metamodel.facets.members.cssclassfa.CssClassFaFacet;
 import 
org.apache.causeway.core.metamodel.facets.members.cssclassfa.annotprop.CssClassFaFacetOnMemberFromConfiguredRegex;
-import 
org.apache.causeway.core.metamodel.postprocessors.ObjectSpecificationPostProcessorAbstract;
+import 
org.apache.causeway.core.metamodel.postprocessors.MetaModelPostProcessorAbstract;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
 
 public class CssOnActionFromConfiguredRegexPostProcessor
-extends ObjectSpecificationPostProcessorAbstract {
+extends MetaModelPostProcessorAbstract {
 
     @Inject
     public CssOnActionFromConfiguredRegexPostProcessor(final MetaModelContext 
mmc) {
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/all/DescribedAsFromTypePostProcessor.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/all/DescribedAsFromTypePostProcessor.java
index 2f6d42efdf..2551e2a807 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/all/DescribedAsFromTypePostProcessor.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/all/DescribedAsFromTypePostProcessor.java
@@ -27,7 +27,7 @@ import 
org.apache.causeway.core.metamodel.facets.all.described.ObjectDescribedFa
 import 
org.apache.causeway.core.metamodel.facets.all.described.ParamDescribedFacet;
 import 
org.apache.causeway.core.metamodel.facets.members.described.annotprop.MemberDescribedFacetFromType;
 import 
org.apache.causeway.core.metamodel.facets.param.described.annotderived.ParamDescribedFacetFromType;
-import 
org.apache.causeway.core.metamodel.postprocessors.ObjectSpecificationPostProcessorAbstract;
+import 
org.apache.causeway.core.metamodel.postprocessors.MetaModelPostProcessorAbstract;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectActionParameter;
@@ -36,7 +36,7 @@ import 
org.apache.causeway.core.metamodel.spec.feature.OneToManyAssociation;
 import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
 
 public class DescribedAsFromTypePostProcessor
-extends ObjectSpecificationPostProcessorAbstract {
+extends MetaModelPostProcessorAbstract {
 
     @Inject
     public DescribedAsFromTypePostProcessor(final MetaModelContext mmc) {
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/all/SanityChecksPostProcessor.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/all/SanityChecksValidator.java
similarity index 50%
rename from 
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/all/SanityChecksPostProcessor.java
rename to 
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/all/SanityChecksValidator.java
index ebdee2e640..87f83fac99 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/all/SanityChecksPostProcessor.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/all/SanityChecksValidator.java
@@ -18,56 +18,111 @@
  */
 package org.apache.causeway.core.metamodel.postprocessors.all;
 
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Stack;
+
 import javax.inject.Inject;
 
+import org.apache.causeway.commons.internal.assertions._Assert;
 import org.apache.causeway.core.config.progmodel.ProgrammingModelConstants;
 import org.apache.causeway.core.metamodel.context.MetaModelContext;
 import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
-import 
org.apache.causeway.core.metamodel.postprocessors.ObjectSpecificationPostProcessorAbstract;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectActionParameter;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectMember;
 import org.apache.causeway.core.metamodel.spec.feature.OneToManyAssociation;
 import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
+import 
org.apache.causeway.core.metamodel.specloader.validator.MetaModelValidatorAbstract;
 import 
org.apache.causeway.core.metamodel.specloader.validator.ValidationFailure;
 
+import lombok.val;
+
 /**
  * Checks various preconditions for a sane meta-model.
  * <ul>
+ * <li>Guard against members and mixed-in members that share the same 
member-id.</li>
  * <li>Guard against members that contribute vetoed or managed types.
  * Those are not allowed as member/return/param.</li>
  * </ul>
  */
-public class SanityChecksPostProcessor
-extends ObjectSpecificationPostProcessorAbstract {
+public class SanityChecksValidator
+extends MetaModelValidatorAbstract {
 
     @Inject
-    public SanityChecksPostProcessor(final MetaModelContext mmc) {
+    public SanityChecksValidator(final MetaModelContext mmc) {
         super(mmc);
     }
 
     @Override
-    public void postProcessParameter(final ObjectSpecification 
objectSpecification, final ObjectAction objectAction, final 
ObjectActionParameter parameter) {
+    public void validateParameter(final ObjectSpecification 
objectSpecification, final ObjectAction objectAction, final 
ObjectActionParameter parameter) {
         checkElementType(parameter, objectSpecification, 
parameter.getElementType());
     }
 
     @Override
-    public void postProcessAction(final ObjectSpecification 
objectSpecification, final ObjectAction objectAction) {
+    public void validateAction(final ObjectSpecification objectSpecification, 
final ObjectAction objectAction) {
+        checkMemberId(objectAction, objectSpecification);
         checkElementType(objectAction, objectSpecification, 
objectAction.getElementType());
     }
 
     @Override
-    public void postProcessProperty(final ObjectSpecification 
objectSpecification, final OneToOneAssociation prop) {
+    public void validateProperty(final ObjectSpecification 
objectSpecification, final OneToOneAssociation prop) {
+        checkMemberId(prop, objectSpecification);
         checkElementType(prop, objectSpecification, prop.getElementType());
     }
 
     @Override
-    public void postProcessCollection(final ObjectSpecification 
objectSpecification, final OneToManyAssociation coll) {
+    public void validateCollection(final ObjectSpecification 
objectSpecification, final OneToManyAssociation coll) {
+        checkMemberId(coll, objectSpecification);
         checkElementType(coll, objectSpecification, coll.getElementType());
     }
 
+    @Override
+    public void validateObjectEnter(final ObjectSpecification objSpec) {
+
+        _Assert.assertTrue(processingStack.isEmpty()); //TODO[CAUSEWAY-3051] 
simplify
+
+        processingStack.push(memberIds = new HashMap<>());
+        //System.err.printf("START %s %s%n", this.getClass().getSimpleName(), 
""+this.hashCode());
+    }
+
+    @Override
+    public void validateObjectExit(final ObjectSpecification objSpec) {
+        memberIds = processingStack.isEmpty()
+                ? null
+                : processingStack.pop(); // garbage collect
+    }
+
     // -- HELPER
 
+    private Map<String, ObjectMember> memberIds;
+    private final Stack<Map<String, ObjectMember>> processingStack = new 
Stack<>();
+
+    private void checkMemberId(
+            final ObjectMember objectMember,
+            final ObjectSpecification declaringType) {
+
+        if(declaringType.isAbstract()) return;
+
+        //TODO[CAUSEWAY-3051] debugging tests
+        
if(objectMember.getDeclaringType().toString().contains("InvalidMemberIdClash")) 
{
+            System.err.printf("member-id: %s (%s)%n", objectMember.getId(), 
objectMember.getDeclaringType());
+        }
+
+        val previous = memberIds.put(objectMember.getId(), objectMember);
+        if(previous!=null) {
+            ValidationFailure.raiseFormatted(objectMember,
+                    ProgrammingModelConstants.Violation.MEMBER_ID_CLASH
+                        .builder()
+                        .addVariable("type", declaringType.fqcn())
+                        .addVariable("memberId", ""+objectMember.getId())
+                        .addVariable("member1", 
previous.getFeatureIdentifier().getFullIdentityString())
+                        .addVariable("member2", 
objectMember.getFeatureIdentifier().getFullIdentityString())
+                        .buildMessage());
+        }
+    }
+
     private void checkElementType(
             final FacetHolder facetHolder,
             final ObjectSpecification declaringType,
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/all/i18n/SynthesizeObjectNamingPostProcessor.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/all/i18n/SynthesizeObjectNamingPostProcessor.java
index e1e562ce58..79cd799c07 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/all/i18n/SynthesizeObjectNamingPostProcessor.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/all/i18n/SynthesizeObjectNamingPostProcessor.java
@@ -28,13 +28,13 @@ import 
org.apache.causeway.core.metamodel.facets.all.i8n.noun.NounForm;
 import org.apache.causeway.core.metamodel.facets.all.i8n.noun.NounForms;
 import org.apache.causeway.core.metamodel.facets.all.named.ObjectNamedFacet;
 import 
org.apache.causeway.core.metamodel.facets.all.named.ObjectNamedFacetSynthesized;
-import 
org.apache.causeway.core.metamodel.postprocessors.ObjectSpecificationPostProcessorAbstract;
+import 
org.apache.causeway.core.metamodel.postprocessors.MetaModelPostProcessorAbstract;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 
 import lombok.val;
 
 public class SynthesizeObjectNamingPostProcessor
-extends ObjectSpecificationPostProcessorAbstract {
+extends MetaModelPostProcessorAbstract {
 
     @Inject
     public SynthesizeObjectNamingPostProcessor(final MetaModelContext 
metaModelContext) {
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/all/i18n/TranslationPostProcessor.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/all/i18n/TranslationPostProcessor.java
index c14c99c0d2..25b160dd73 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/all/i18n/TranslationPostProcessor.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/all/i18n/TranslationPostProcessor.java
@@ -32,7 +32,7 @@ import 
org.apache.causeway.core.metamodel.facets.all.i8n.HasMemoizableTranslatio
 import org.apache.causeway.core.metamodel.facets.all.named.MemberNamedFacet;
 import org.apache.causeway.core.metamodel.facets.all.named.ObjectNamedFacet;
 import org.apache.causeway.core.metamodel.facets.all.named.ParamNamedFacet;
-import 
org.apache.causeway.core.metamodel.postprocessors.ObjectSpecificationPostProcessorAbstract;
+import 
org.apache.causeway.core.metamodel.postprocessors.MetaModelPostProcessorAbstract;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectActionParameter;
@@ -40,7 +40,7 @@ import 
org.apache.causeway.core.metamodel.spec.feature.OneToManyAssociation;
 import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
 
 public class TranslationPostProcessor
-extends ObjectSpecificationPostProcessorAbstract {
+extends MetaModelPostProcessorAbstract {
 
     @Inject
     public TranslationPostProcessor(final MetaModelContext metaModelContext) {
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/allbutparam/authorization/AuthorizationPostProcessor.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/allbutparam/authorization/AuthorizationPostProcessor.java
index c77b54fdc5..f3b4bb1c65 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/allbutparam/authorization/AuthorizationPostProcessor.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/allbutparam/authorization/AuthorizationPostProcessor.java
@@ -22,14 +22,14 @@ import javax.inject.Inject;
 
 import org.apache.causeway.core.metamodel.context.MetaModelContext;
 import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
-import 
org.apache.causeway.core.metamodel.postprocessors.ObjectSpecificationPostProcessorAbstract;
+import 
org.apache.causeway.core.metamodel.postprocessors.MetaModelPostProcessorAbstract;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
 import org.apache.causeway.core.metamodel.spec.feature.OneToManyAssociation;
 import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
 
 public class AuthorizationPostProcessor
-    extends ObjectSpecificationPostProcessorAbstract {
+    extends MetaModelPostProcessorAbstract {
 
     @Inject
     public AuthorizationPostProcessor(final MetaModelContext metaModelContext) 
{
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/members/SynthesizeDomainEventsForMixinPostProcessor.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/members/SynthesizeDomainEventsForMixinPostProcessor.java
index 32d14e9f09..3123813c9f 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/members/SynthesizeDomainEventsForMixinPostProcessor.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/members/SynthesizeDomainEventsForMixinPostProcessor.java
@@ -24,7 +24,7 @@ import 
org.apache.causeway.core.metamodel.context.MetaModelContext;
 import 
org.apache.causeway.core.metamodel.facets.actions.action.invocation.ActionDomainEventFacet;
 import 
org.apache.causeway.core.metamodel.facets.collections.collection.modify.CollectionDomainEventFacet;
 import 
org.apache.causeway.core.metamodel.facets.properties.property.modify.PropertyDomainEventFacet;
-import 
org.apache.causeway.core.metamodel.postprocessors.ObjectSpecificationPostProcessorAbstract;
+import 
org.apache.causeway.core.metamodel.postprocessors.MetaModelPostProcessorAbstract;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
 import org.apache.causeway.core.metamodel.spec.feature.OneToManyAssociation;
@@ -37,7 +37,7 @@ import 
org.apache.causeway.core.metamodel.specloader.validator.ValidationFailure
  * unless overwritten by the mixin type.
  */
 public class SynthesizeDomainEventsForMixinPostProcessor
-extends ObjectSpecificationPostProcessorAbstract {
+extends MetaModelPostProcessorAbstract {
 
     @Inject
     public SynthesizeDomainEventsForMixinPostProcessor(final MetaModelContext 
metaModelContext) {
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/members/navigation/NavigationFacetFromHiddenTypePostProcessor.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/members/navigation/NavigationFacetFromHiddenTypePostProcessor.java
index 8fcce81eb6..61c7a8e7d6 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/members/navigation/NavigationFacetFromHiddenTypePostProcessor.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/members/navigation/NavigationFacetFromHiddenTypePostProcessor.java
@@ -24,7 +24,7 @@ import 
org.apache.causeway.core.metamodel.context.MetaModelContext;
 import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
 import org.apache.causeway.core.metamodel.facetapi.FacetUtil;
 import org.apache.causeway.core.metamodel.facets.object.hidden.HiddenTypeFacet;
-import 
org.apache.causeway.core.metamodel.postprocessors.ObjectSpecificationPostProcessorAbstract;
+import 
org.apache.causeway.core.metamodel.postprocessors.MetaModelPostProcessorAbstract;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectMember;
@@ -35,7 +35,7 @@ import 
org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
  * Installs the {@link NavigationFacetFromHiddenType} on all of the
  * {@link ObjectMember}s of the {@link ObjectSpecification}.
  */
-public class NavigationFacetFromHiddenTypePostProcessor extends 
ObjectSpecificationPostProcessorAbstract {
+public class NavigationFacetFromHiddenTypePostProcessor extends 
MetaModelPostProcessorAbstract {
 
     @Inject
     public NavigationFacetFromHiddenTypePostProcessor(final MetaModelContext 
metaModelContext) {
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/object/ProjectionFacetsPostProcessor.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/object/ProjectionFacetsPostProcessor.java
index dec1600115..ceb448fc05 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/object/ProjectionFacetsPostProcessor.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/object/ProjectionFacetsPostProcessor.java
@@ -32,13 +32,13 @@ import 
org.apache.causeway.core.metamodel.facets.object.projection.ident.CssClas
 import 
org.apache.causeway.core.metamodel.facets.object.projection.ident.IconFacetFromProjectionFacet;
 import 
org.apache.causeway.core.metamodel.facets.object.projection.ident.TitleFacetFromProjectionFacet;
 import org.apache.causeway.core.metamodel.facets.object.title.TitleFacet;
-import 
org.apache.causeway.core.metamodel.postprocessors.ObjectSpecificationPostProcessorAbstract;
+import 
org.apache.causeway.core.metamodel.postprocessors.MetaModelPostProcessorAbstract;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 
 import lombok.val;
 
 public class ProjectionFacetsPostProcessor
-extends ObjectSpecificationPostProcessorAbstract {
+extends MetaModelPostProcessorAbstract {
 
     @Inject
     public ProjectionFacetsPostProcessor(final MetaModelContext 
metaModelContext) {
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/param/ChoicesAndDefaultsPostProcessor.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/param/ChoicesAndDefaultsPostProcessor.java
index a6a07b29d2..1178cd5c97 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/param/ChoicesAndDefaultsPostProcessor.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/param/ChoicesAndDefaultsPostProcessor.java
@@ -35,7 +35,7 @@ import 
org.apache.causeway.core.metamodel.facets.properties.choices.PropertyChoi
 import 
org.apache.causeway.core.metamodel.facets.properties.choices.enums.PropertyChoicesFacetFromChoicesFacet;
 import 
org.apache.causeway.core.metamodel.facets.properties.defaults.PropertyDefaultFacet;
 import 
org.apache.causeway.core.metamodel.facets.properties.defaults.fromtype.PropertyDefaultFacetFromDefaultedFacet;
-import 
org.apache.causeway.core.metamodel.postprocessors.ObjectSpecificationPostProcessorAbstract;
+import 
org.apache.causeway.core.metamodel.postprocessors.MetaModelPostProcessorAbstract;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
@@ -53,7 +53,7 @@ import lombok.val;
  *
  */
 public class ChoicesAndDefaultsPostProcessor
-extends ObjectSpecificationPostProcessorAbstract {
+extends MetaModelPostProcessorAbstract {
 
     @Inject
     public ChoicesAndDefaultsPostProcessor(final MetaModelContext 
metaModelContext) {
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/param/TypicalLengthFromTypePostProcessor.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/param/TypicalLengthFromTypePostProcessor.java
index ae16377fda..de4a46e80e 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/param/TypicalLengthFromTypePostProcessor.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/param/TypicalLengthFromTypePostProcessor.java
@@ -25,14 +25,14 @@ import 
org.apache.causeway.core.metamodel.facetapi.FacetUtil;
 import 
org.apache.causeway.core.metamodel.facets.objectvalue.typicallen.TypicalLengthFacet;
 import 
org.apache.causeway.core.metamodel.facets.param.typicallen.fromtype.TypicalLengthFacetOnParameterFromType;
 import 
org.apache.causeway.core.metamodel.facets.properties.typicallen.fromtype.TypicalLengthFacetOnPropertyFromType;
-import 
org.apache.causeway.core.metamodel.postprocessors.ObjectSpecificationPostProcessorAbstract;
+import 
org.apache.causeway.core.metamodel.postprocessors.MetaModelPostProcessorAbstract;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectActionParameter;
 import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
 
 public class TypicalLengthFromTypePostProcessor
-extends ObjectSpecificationPostProcessorAbstract {
+extends MetaModelPostProcessorAbstract {
 
     @Inject
     public TypicalLengthFromTypePostProcessor(final MetaModelContext 
metaModelContext) {
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/properties/DisabledFromImmutablePostProcessor.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/properties/DisabledFromImmutablePostProcessor.java
index 06f2d211b9..2335b7e8ff 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/properties/DisabledFromImmutablePostProcessor.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/postprocessors/properties/DisabledFromImmutablePostProcessor.java
@@ -28,7 +28,7 @@ import 
org.apache.causeway.core.metamodel.facets.object.immutable.EditingEnabled
 import 
org.apache.causeway.core.metamodel.facets.object.immutable.ImmutableFacet;
 import 
org.apache.causeway.core.metamodel.facets.properties.disabled.fromimmutable.DisabledFacetOnPropertyFromImmutable;
 import 
org.apache.causeway.core.metamodel.facets.properties.disabled.fromimmutable.DisabledFacetOnPropertyFromImmutableFactory;
-import 
org.apache.causeway.core.metamodel.postprocessors.ObjectSpecificationPostProcessorAbstract;
+import 
org.apache.causeway.core.metamodel.postprocessors.MetaModelPostProcessorAbstract;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
 import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
@@ -40,7 +40,7 @@ import lombok.val;
  * Replaces {@link DisabledFacetOnPropertyFromImmutableFactory}
  */
 public class DisabledFromImmutablePostProcessor
-extends ObjectSpecificationPostProcessorAbstract {
+extends MetaModelPostProcessorAbstract {
 
     @Inject
     public DisabledFromImmutablePostProcessor(final MetaModelContext 
metaModelContext) {
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/progmodel/ProgrammingModel.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/progmodel/ProgrammingModel.java
index 5ab71fc8cf..95e27f764a 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/progmodel/ProgrammingModel.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/progmodel/ProgrammingModel.java
@@ -23,10 +23,10 @@ import java.util.stream.Stream;
 
 import org.apache.causeway.core.metamodel.context.HasMetaModelContext;
 import org.apache.causeway.core.metamodel.facets.FacetFactory;
-import 
org.apache.causeway.core.metamodel.postprocessors.ObjectSpecificationPostProcessor;
+import 
org.apache.causeway.core.metamodel.postprocessors.MetaModelPostProcessor;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import 
org.apache.causeway.core.metamodel.specloader.validator.MetaModelValidator;
-import 
org.apache.causeway.core.metamodel.specloader.validator.MetaModelVisitingValidatorAbstract;
+import 
org.apache.causeway.core.metamodel.specloader.validator.MetaModelValidatorAbstract;
 
 import lombok.NonNull;
 
@@ -123,7 +123,7 @@ extends HasMetaModelContext {
             T instance,
             Marker ... markers);
 
-    <T extends ObjectSpecificationPostProcessor> void addPostProcessor(
+    <T extends MetaModelPostProcessor> void addPostProcessor(
             PostProcessingOrder order,
             T instance,
             Marker ... markers);
@@ -131,7 +131,7 @@ extends HasMetaModelContext {
 
     Stream<FacetFactory> streamFactories();
     Stream<MetaModelValidator> streamValidators();
-    Stream<ObjectSpecificationPostProcessor> streamPostProcessors();
+    Stream<MetaModelPostProcessor> streamPostProcessors();
 
     // -- SHORTCUTS
 
@@ -147,9 +147,9 @@ extends HasMetaModelContext {
             final @NonNull Consumer<ObjectSpecification> validator,
             final Marker ... markers) {
 
-        addValidator(new 
MetaModelVisitingValidatorAbstract(getMetaModelContext()) {
+        addValidator(new MetaModelValidatorAbstract(getMetaModelContext()) {
             @Override
-            public void validate(final @NonNull ObjectSpecification spec) {
+            public void validateObjectEnter(final @NonNull ObjectSpecification 
spec) {
                 validator.accept(spec);
             }
         });
@@ -159,9 +159,9 @@ extends HasMetaModelContext {
             final @NonNull Consumer<ObjectSpecification> validator,
             final Marker ... markers) {
 
-        addValidator(new 
MetaModelVisitingValidatorAbstract(getMetaModelContext()) {
+        addValidator(new MetaModelValidatorAbstract(getMetaModelContext()) {
             @Override
-            public void validate(final @NonNull ObjectSpecification spec) {
+            public void validateObjectEnter(final @NonNull ObjectSpecification 
spec) {
                 if(spec.isInjectable()) {
                     return;
                 }
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/progmodel/ProgrammingModelAbstract.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/progmodel/ProgrammingModelAbstract.java
index 6f1db4990a..87f0312f7c 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/progmodel/ProgrammingModelAbstract.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/progmodel/ProgrammingModelAbstract.java
@@ -31,7 +31,7 @@ import 
org.apache.causeway.core.metamodel.context.HasMetaModelContext;
 import org.apache.causeway.core.metamodel.context.MetaModelContext;
 import org.apache.causeway.core.metamodel.facetapi.MetaModelRefiner;
 import org.apache.causeway.core.metamodel.facets.FacetFactory;
-import 
org.apache.causeway.core.metamodel.postprocessors.ObjectSpecificationPostProcessor;
+import 
org.apache.causeway.core.metamodel.postprocessors.MetaModelPostProcessor;
 import 
org.apache.causeway.core.metamodel.specloader.validator.MetaModelValidator;
 
 import lombok.EqualsAndHashCode;
@@ -49,7 +49,7 @@ implements
 
     private List<FacetFactory> unmodifiableFactories;
     private List<MetaModelValidator> unmodifiableValidators;
-    private List<ObjectSpecificationPostProcessor> unmodifiablePostProcessors;
+    private List<MetaModelPostProcessor> unmodifiablePostProcessors;
 
     protected ProgrammingModelAbstract(final MetaModelContext 
metaModelContext) {
         this.metaModelContext = metaModelContext;
@@ -109,7 +109,7 @@ implements
     }
 
     @Override
-    public <T extends ObjectSpecificationPostProcessor> void addPostProcessor(
+    public <T extends MetaModelPostProcessor> void addPostProcessor(
             final PostProcessingOrder order,
             final T instance,
             final Marker... markers) {
@@ -135,7 +135,7 @@ implements
     }
 
     @Override
-    public Stream<ObjectSpecificationPostProcessor> streamPostProcessors() {
+    public Stream<MetaModelPostProcessor> streamPostProcessors() {
         assertInitialized();
         return unmodifiablePostProcessors.stream();
     }
@@ -194,14 +194,14 @@ implements
         return validators;
     }
 
-    private final SetMultimap<PostProcessingOrder, ProgrammingModelEntry<? 
extends ObjectSpecificationPostProcessor>>
+    private final SetMultimap<PostProcessingOrder, ProgrammingModelEntry<? 
extends MetaModelPostProcessor>>
         postProcessorEntriesByOrder = 
_Multimaps.newSetMultimap(LinkedHashSet::new);
 
-    private List<ObjectSpecificationPostProcessor> snapshotPostProcessors(
+    private List<MetaModelPostProcessor> snapshotPostProcessors(
             final ProgrammingModelInitFilter filter,
             final MetaModelContext metaModelContext) {
 
-        val postProcessors = 
_Lists.<ObjectSpecificationPostProcessor>newArrayList();
+        val postProcessors = _Lists.<MetaModelPostProcessor>newArrayList();
         for(val order : PostProcessingOrder.values()) {
             val postProcessorEntrySet = postProcessorEntriesByOrder.get(order);
             if(postProcessorEntrySet==null) {
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/progmodel/ProgrammingModelInitFilter.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/progmodel/ProgrammingModelInitFilter.java
index 89412621b4..36c77e2909 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/progmodel/ProgrammingModelInitFilter.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/progmodel/ProgrammingModelInitFilter.java
@@ -25,7 +25,7 @@ import org.springframework.lang.Nullable;
 
 import org.apache.causeway.commons.internal.functions._Predicates;
 import org.apache.causeway.core.metamodel.facets.FacetFactory;
-import 
org.apache.causeway.core.metamodel.postprocessors.ObjectSpecificationPostProcessor;
+import 
org.apache.causeway.core.metamodel.postprocessors.MetaModelPostProcessor;
 import 
org.apache.causeway.core.metamodel.specloader.validator.MetaModelValidator;
 
 import static org.apache.causeway.commons.internal.base._NullSafe.isEmpty;
@@ -48,7 +48,7 @@ public interface ProgrammingModelInitFilter {
             ProgrammingModel.Marker[] markersIfAny);
 
     boolean acceptPostProcessor(
-            Class<? extends ObjectSpecificationPostProcessor> 
postProcessorType,
+            Class<? extends MetaModelPostProcessor> postProcessorType,
             ProgrammingModel.Marker[] markersIfAny);
 
     // -- PREDEFINED PREDICATES
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/progmodel/ProgrammingModelInitFilterDefault.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/progmodel/ProgrammingModelInitFilterDefault.java
index 6110da8ac9..a3ebd4b8b8 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/progmodel/ProgrammingModelInitFilterDefault.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/progmodel/ProgrammingModelInitFilterDefault.java
@@ -30,7 +30,7 @@ import org.springframework.stereotype.Component;
 import org.apache.causeway.core.config.CausewayConfiguration;
 import org.apache.causeway.core.metamodel.CausewayModuleCoreMetamodel;
 import org.apache.causeway.core.metamodel.facets.FacetFactory;
-import 
org.apache.causeway.core.metamodel.postprocessors.ObjectSpecificationPostProcessor;
+import 
org.apache.causeway.core.metamodel.postprocessors.MetaModelPostProcessor;
 import 
org.apache.causeway.core.metamodel.specloader.validator.MetaModelValidator;
 
 import static 
org.apache.causeway.core.metamodel.progmodel.ProgrammingModelInitFilter.excluding;
@@ -79,7 +79,7 @@ public class ProgrammingModelInitFilterDefault implements 
ProgrammingModelInitFi
 
     @Override
     public boolean acceptPostProcessor(
-            Class<? extends ObjectSpecificationPostProcessor> 
postProcessorType,
+            Class<? extends MetaModelPostProcessor> postProcessorType,
             ProgrammingModel.Marker[] markersIfAny) {
 
         return filterOnMarker.test(markersIfAny);
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/progmodels/dflt/ProgrammingModelFacetsJava11.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/progmodels/dflt/ProgrammingModelFacetsJava11.java
index 39eb78ed14..a2bbacdc4f 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/progmodels/dflt/ProgrammingModelFacetsJava11.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/progmodels/dflt/ProgrammingModelFacetsJava11.java
@@ -84,7 +84,7 @@ import 
org.apache.causeway.core.metamodel.facets.value.semantics.ValueSemanticsA
 import 
org.apache.causeway.core.metamodel.methods.DomainIncludeAnnotationEnforcesMetamodelContributionValidator;
 import 
org.apache.causeway.core.metamodel.postprocessors.all.CssOnActionFromConfiguredRegexPostProcessor;
 import 
org.apache.causeway.core.metamodel.postprocessors.all.DescribedAsFromTypePostProcessor;
-import 
org.apache.causeway.core.metamodel.postprocessors.all.SanityChecksPostProcessor;
+import 
org.apache.causeway.core.metamodel.postprocessors.all.SanityChecksValidator;
 import 
org.apache.causeway.core.metamodel.postprocessors.all.i18n.SynthesizeObjectNamingPostProcessor;
 import 
org.apache.causeway.core.metamodel.postprocessors.all.i18n.TranslationPostProcessor;
 import 
org.apache.causeway.core.metamodel.postprocessors.allbutparam.authorization.AuthorizationPostProcessor;
@@ -239,7 +239,6 @@ extends ProgrammingModelAbstract {
     private void addPostProcessors() {
 
         val mmc = getMetaModelContext();
-        addPostProcessor(PostProcessingOrder.A1_BUILTIN, new 
SanityChecksPostProcessor(mmc));
 
         // must run before Object nouns are used
         addPostProcessor(PostProcessingOrder.A1_BUILTIN, new 
SynthesizeObjectNamingPostProcessor(mmc));
@@ -265,6 +264,7 @@ extends ProgrammingModelAbstract {
 
         val mmc = getMetaModelContext();
 
+        addValidator(new SanityChecksValidator(mmc));
         addValidator(new 
DomainIncludeAnnotationEnforcesMetamodelContributionValidator(mmc));
         addValidator(new TitlesAndTranslationsValidator(mmc));  // should this 
instead be a post processor, alongside TranslationPostProcessor ?
         addValidator(new 
ActionAnnotationShouldEnforceConcreteTypeToBeIncludedWithMetamodelValidator(mmc));
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/title/TitlesAndTranslationsValidator.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/title/TitlesAndTranslationsValidator.java
index 1e2f0c0c0f..541646924e 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/title/TitlesAndTranslationsValidator.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/title/TitlesAndTranslationsValidator.java
@@ -23,7 +23,6 @@ import javax.inject.Inject;
 import org.apache.causeway.applib.Identifier;
 import org.apache.causeway.applib.id.LogicalType;
 import org.apache.causeway.applib.services.i18n.TranslationContext;
-import org.apache.causeway.applib.services.title.TitleService;
 import org.apache.causeway.commons.internal.base._Blackhole;
 import org.apache.causeway.core.config.messages.MessageRegistry;
 import org.apache.causeway.core.metamodel.context.MetaModelContext;
@@ -46,7 +45,7 @@ extends MetaModelValidatorAbstract {
     }
 
     @Override
-    public void validate() {
+    public void validateEnter() {
         validateServiceTitles();
         validateEnumTitles();
         validateRegisteredMessageTranslation();
@@ -54,10 +53,9 @@ extends MetaModelValidatorAbstract {
 
     private void validateServiceTitles() {
 
-        val serviceRegistry = super.getMetaModelContext().getServiceRegistry();
-        val specificationLoader = 
super.getMetaModelContext().getSpecificationLoader();
-        val titleService = 
serviceRegistry.lookupServiceElseFail(TitleService.class);
-
+        val serviceRegistry = getServiceRegistry();
+        val specificationLoader = getSpecificationLoader();
+        val titleService = getTitleService();
 
         serviceRegistry.streamRegisteredBeans()
         .forEach(managedBeanAdapter->{
@@ -109,9 +107,8 @@ extends MetaModelValidatorAbstract {
 
     private void validateEnumTitles() {
 
-        val serviceRegistry = super.getMetaModelContext().getServiceRegistry();
-        val specificationLoader = 
super.getMetaModelContext().getSpecificationLoader();
-        val titleService = 
serviceRegistry.lookupServiceElseFail(TitleService.class);
+        val specificationLoader = getSpecificationLoader();
+        val titleService = getTitleService();
 
         // (previously we took a protective copy to avoid a concurrent 
modification exception,
         // but this is now done by SpecificationLoader itself)
@@ -147,11 +144,8 @@ extends MetaModelValidatorAbstract {
 
     private void validateRegisteredMessageTranslation() {
 
-        val specificationLoader = 
super.getMetaModelContext().getSpecificationLoader();
-        val translationService = 
super.getMetaModelContext().getTranslationService();
-
-        // as used by the Wicket UI?
-        // final TranslationContext context = 
"org.apache.causeway...InteractionService";
+        val specificationLoader = getSpecificationLoader();
+        val translationService = getTranslationService();
 
         // see @ConfirmUiModel#translate()
         val translationContext = 
TranslationContext.forClassName(MessageRegistry.class);
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/SpecificationLoaderDefault.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/SpecificationLoaderDefault.java
index e40e3c6fde..a7228357a4 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/SpecificationLoaderDefault.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/SpecificationLoaderDefault.java
@@ -80,7 +80,6 @@ import 
org.apache.causeway.core.metamodel.specloader.facetprocessor.FacetProcess
 import 
org.apache.causeway.core.metamodel.specloader.postprocessor.PostProcessor;
 import 
org.apache.causeway.core.metamodel.specloader.specimpl.IntrospectionState;
 import 
org.apache.causeway.core.metamodel.specloader.specimpl.dflt.ObjectSpecificationDefault;
-import 
org.apache.causeway.core.metamodel.specloader.validator.MetaModelValidatorAbstract;
 import 
org.apache.causeway.core.metamodel.specloader.validator.ValidationFailure;
 import 
org.apache.causeway.core.metamodel.specloader.validator.ValidationFailures;
 import 
org.apache.causeway.core.metamodel.valuetypes.ValueSemanticsResolverDefault;
@@ -288,7 +287,7 @@ implements
         //XXX[CAUSEWAY-2382] when parallel introspecting, make sure we have 
the mixins before their holders
         // (observation by experiment, no real understanding as to why)
 
-        _Util.logBefore(log, cache, knownSpecs);
+        _LogUtil.logBefore(log, cache, knownSpecs);
 
         log.info(" - introspecting {} type hierarchies", knownSpecs.size());
         introspect(Can.ofCollection(knownSpecs), 
IntrospectionState.TYPE_INTROSPECTED);
@@ -312,7 +311,7 @@ implements
 
         introspect(Can.ofCollection(domainObjectSpecs), 
IntrospectionState.FULLY_INTROSPECTED);
 
-        _Util.logAfter(log, cache, knownSpecs);
+        _LogUtil.logAfter(log, cache, knownSpecs);
 
         if(isFullIntrospect()) {
             val snapshot = cache.snapshotSpecs();
@@ -370,7 +369,7 @@ implements
         log.debug("shutting down {}", this);
         disposeMetaModel();
         facetProcessor.shutdown();
-        postProcessor.shutdown();
+        postProcessor.close();
         facetProcessor = null;
     }
 
@@ -511,29 +510,14 @@ implements
     }
 
     private _Lazy<ValidationFailures> validationResult =
-            _Lazy.threadSafe(this::collectFailuresFromMetaModel);
+            _Lazy.threadSafe(this::runMetaModelValidators);
 
     private final AtomicBoolean validationInProgress = new 
AtomicBoolean(false);
     private final BlockingQueue<ObjectSpecification> validationQueue = new 
LinkedBlockingQueue<>();
 
-    private ValidationFailures collectFailuresFromMetaModel() {
+    private ValidationFailures runMetaModelValidators() {
         validationInProgress.set(true);
-
-        programmingModel.streamValidators()
-        .map(MetaModelValidatorAbstract.class::cast)
-        .forEach(validator -> {
-            log.debug("Running validator: {}", validator);
-            try {
-                validator.validate();
-            } catch (Throwable t) {
-                log.error(t);
-                throw t;
-            } finally {
-                log.debug("Done validator: {}", validator);
-            }
-        });
-
-        log.debug("Done");
+        _ValidateUtil.runValidators(programmingModel, this);
         validationInProgress.set(false);
 
         return validationFailures;
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/_Util.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/_LogUtil.java
similarity index 99%
rename from 
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/_Util.java
rename to 
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/_LogUtil.java
index df9f7fb8f1..642fe881cd 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/_Util.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/_LogUtil.java
@@ -30,7 +30,7 @@ import lombok.val;
 import lombok.experimental.UtilityClass;
 
 @UtilityClass
-final class _Util {
+final class _LogUtil {
 
     void logBefore(
             final Logger log,
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/_ValidateUtil.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/_ValidateUtil.java
new file mode 100644
index 0000000000..7a1c7c4a88
--- /dev/null
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/_ValidateUtil.java
@@ -0,0 +1,88 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.core.metamodel.specloader;
+
+import org.apache.causeway.commons.collections.Can;
+import org.apache.causeway.core.metamodel.progmodel.ProgrammingModel;
+import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
+import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
+import 
org.apache.causeway.core.metamodel.specloader.validator.MetaModelValidator;
+
+import lombok.val;
+import lombok.experimental.UtilityClass;
+import lombok.extern.log4j.Log4j2;
+
+@UtilityClass
+@Log4j2
+class _ValidateUtil{
+
+    void runValidators(
+            final ProgrammingModel programmingModel,
+            final SpecificationLoader specLoader) {
+
+        log.debug("Running MetaModelValidators ...");
+
+        val snapshot = specLoader.snapshotSpecifications();
+
+        programmingModel.streamValidators()
+        .filter(MetaModelValidator::isEnabled)
+        .forEach(validator -> {
+            log.debug("Running validator: {}", validator);
+            try {
+                runValidator(validator, snapshot);
+            } catch (Throwable t) {
+                log.error(t);
+                throw t;
+            } finally {
+                log.debug("Done validator: {}", validator);
+            }
+        });
+
+        log.debug("Done running MetaModelValidators.");
+    }
+
+
+    // -- HELPER
+
+    private void runValidator(final MetaModelValidator validator, final 
Can<ObjectSpecification> snapshot) {
+        validator.validateEnter();
+        snapshot.forEach(objSpec->runValidator(validator, objSpec));
+        validator.validateExit();
+    }
+
+    private void runValidator(final MetaModelValidator validator, final 
ObjectSpecification objSpec) {
+        validator.validateObjectEnter(objSpec);
+
+        objSpec.streamRuntimeActions(MixedIn.INCLUDED)
+        .forEach(act->{
+            act.streamParameters().forEach(param ->
+                validator.validateParameter(objSpec, act, param));
+            validator.validateAction(objSpec, act);
+        });
+
+        objSpec.streamProperties(MixedIn.INCLUDED)
+        .forEach(prop->validator.validateProperty(objSpec, prop));
+
+        objSpec.streamCollections(MixedIn.INCLUDED)
+        .forEach(coll->validator.validateCollection(objSpec, coll));
+
+        validator.validateObjectExit(objSpec);
+    }
+
+}
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/postprocessor/PostProcessor.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/postprocessor/PostProcessor.java
index c461b9ff4f..2c70cb7d36 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/postprocessor/PostProcessor.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/postprocessor/PostProcessor.java
@@ -19,84 +19,49 @@
 package org.apache.causeway.core.metamodel.specloader.postprocessor;
 
 import org.apache.causeway.commons.collections.Can;
-import 
org.apache.causeway.core.metamodel.postprocessors.ObjectSpecificationPostProcessor;
+import 
org.apache.causeway.core.metamodel.postprocessors.MetaModelPostProcessor;
 import org.apache.causeway.core.metamodel.progmodel.ProgrammingModel;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
-import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
-import org.apache.causeway.core.metamodel.spec.feature.OneToManyAssociation;
-import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
 
 import lombok.RequiredArgsConstructor;
 import lombok.val;
 
 @RequiredArgsConstructor
-public class PostProcessor {
+public class PostProcessor implements AutoCloseable {
 
     private final ProgrammingModel programmingModel;
-    private Can<ObjectSpecificationPostProcessor> enabledPostProcessors = 
Can.empty(); // populated at #init
+    private Can<MetaModelPostProcessor> enabledPostProcessors = Can.empty(); 
// populated at #init
 
     public void init() {
         enabledPostProcessors = programmingModel.streamPostProcessors()
-                .filter(ObjectSpecificationPostProcessor::isEnabled)
+                .filter(MetaModelPostProcessor::isEnabled)
                 .collect(Can.toCan());
     }
 
-    public void shutdown() {
+    @Override
+    public void close() {
         enabledPostProcessors = Can.empty();
     }
 
     public void postProcess(final ObjectSpecification objectSpecification) {
-        // calling count on these 3 streams so these are actually consumed,
-        // as a side-effect the meta-model potentially gets further populated
-//        objectSpecification.streamRuntimeActions(MixedIn.INCLUDED).count();
-//        objectSpecification.streamCollections(MixedIn.INCLUDED).count();
-//        objectSpecification.streamProperties(MixedIn.INCLUDED).count();
 
-        postProcessObject(objectSpecification);
-
-        objectSpecification.streamRuntimeActions(MixedIn.INCLUDED)
-        .forEach(act->postProcessAction(objectSpecification, act));
-
-        objectSpecification.streamProperties(MixedIn.INCLUDED)
-        .forEach(prop->postProcessProperty(objectSpecification, prop));
-
-        objectSpecification.streamCollections(MixedIn.INCLUDED)
-        .forEach(coll->postProcessCollection(objectSpecification, coll));
-    }
-
-    // -- HELPER
-
-    private void postProcessObject(
-            final ObjectSpecification objectSpecification) {
         for (val postProcessor : enabledPostProcessors) {
             postProcessor.postProcessObject(objectSpecification);
-        }
-    }
 
-    private void postProcessAction(
-            final ObjectSpecification objectSpecification,
-            final ObjectAction act) {
-        for (val postProcessor : enabledPostProcessors) {
-            act.streamParameters().forEach(param ->
-                postProcessor.postProcessParameter(objectSpecification, act, 
param));
-            postProcessor.postProcessAction(objectSpecification, act);
-        }
-    }
+            objectSpecification.streamRuntimeActions(MixedIn.INCLUDED)
+            .forEach(act->{
+                act.streamParameters().forEach(param ->
+                    postProcessor.postProcessParameter(objectSpecification, 
act, param));
+                postProcessor.postProcessAction(objectSpecification, act);
+            });
 
-    private void postProcessProperty(
-            final ObjectSpecification objectSpecification,
-            final OneToOneAssociation prop) {
-        for (val postProcessor : enabledPostProcessors) {
-            postProcessor.postProcessProperty(objectSpecification, prop);
-        }
-    }
+            objectSpecification.streamProperties(MixedIn.INCLUDED)
+            
.forEach(prop->postProcessor.postProcessProperty(objectSpecification, prop));
+
+            objectSpecification.streamCollections(MixedIn.INCLUDED)
+            
.forEach(coll->postProcessor.postProcessCollection(objectSpecification, coll));
 
-    private void postProcessCollection(
-            final ObjectSpecification objectSpecification,
-            final OneToManyAssociation coll) {
-        for (val postProcessor : enabledPostProcessors) {
-            postProcessor.postProcessCollection(objectSpecification, coll);
         }
     }
 
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/validator/MetaModelValidator.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/validator/MetaModelValidator.java
index 2fd357115d..4f0047e3d6 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/validator/MetaModelValidator.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/validator/MetaModelValidator.java
@@ -18,8 +18,40 @@
  */
 package org.apache.causeway.core.metamodel.specloader.validator;
 
+import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectActionParameter;
+import org.apache.causeway.core.metamodel.spec.feature.OneToManyAssociation;
+import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
+
 public interface MetaModelValidator {
 
-    void validate();
+    default boolean isEnabled() {
+        return true;
+    }
+
+    /** entry to meta-model validation */
+    default void validateEnter() {}
+
+    /** exit from meta-model validation */
+    default void validateExit() {}
+
+    /** entry to validation of specified {@code objSpec} */
+    default void validateObjectEnter(final ObjectSpecification objSpec) {}
+
+    /** exit from validation of specified {@code objSpec} */
+    default void validateObjectExit(final ObjectSpecification objSpec) {}
+
+    /** validate action - mixed-in included */
+    default void validateAction(final ObjectSpecification objSpec, final 
ObjectAction act) {}
+
+    /** validate action-parameter - mixed-in included */
+    default void validateParameter(final ObjectSpecification objSpec, final 
ObjectAction act, final ObjectActionParameter param) {}
+
+    /** validate property - mixed-in included */
+    default void validateProperty(final ObjectSpecification objSpec, final 
OneToOneAssociation prop) {}
+
+    /** validate collection - mixed-in included */
+    default void validateCollection(final ObjectSpecification objSpec, final 
OneToManyAssociation coll) {}
 
 }
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/validator/MetaModelVisitingValidator.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/validator/MetaModelVisitingValidator.java
index 92127059d2..99b10588f7 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/validator/MetaModelVisitingValidator.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/validator/MetaModelVisitingValidator.java
@@ -31,8 +31,6 @@ public interface MetaModelVisitingValidator {
 
     void validate(@NonNull ObjectSpecification spec);
 
-    default void summarize() {
-
-    }
+    default void summarize() {}
 
 }
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/validator/MetaModelVisitingValidatorAbstract.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/validator/MetaModelVisitingValidatorAbstract.java
deleted file mode 100644
index 41e4045202..0000000000
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/validator/MetaModelVisitingValidatorAbstract.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-package org.apache.causeway.core.metamodel.specloader.validator;
-
-import org.apache.causeway.core.metamodel.context.MetaModelContext;
-
-public abstract class MetaModelVisitingValidatorAbstract
-extends MetaModelValidatorAbstract
-implements MetaModelVisitingValidator {
-
-    protected MetaModelVisitingValidatorAbstract(final MetaModelContext 
metaModelContext) {
-        super(metaModelContext);
-    }
-
-    @Override
-    public final void validate() {
-
-        if(!isEnabled()){
-            return;
-        }
-
-        getMetaModelContext().getSpecificationLoader()
-        .forEach(this::validate);
-
-        summarize();
-
-    }
-
-}
diff --git 
a/extensions/security/secman/integration/src/main/java/org/apache/causeway/extensions/secman/integration/facets/TenantedAuthorizationPostProcessor.java
 
b/extensions/security/secman/integration/src/main/java/org/apache/causeway/extensions/secman/integration/facets/TenantedAuthorizationPostProcessor.java
index 6f4a383dad..114b979f93 100644
--- 
a/extensions/security/secman/integration/src/main/java/org/apache/causeway/extensions/secman/integration/facets/TenantedAuthorizationPostProcessor.java
+++ 
b/extensions/security/secman/integration/src/main/java/org/apache/causeway/extensions/secman/integration/facets/TenantedAuthorizationPostProcessor.java
@@ -37,7 +37,7 @@ import 
org.apache.causeway.core.metamodel.context.MetaModelContext;
 import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
 import org.apache.causeway.core.metamodel.facetapi.FacetUtil;
 import org.apache.causeway.core.metamodel.facetapi.MetaModelRefiner;
-import 
org.apache.causeway.core.metamodel.postprocessors.ObjectSpecificationPostProcessorAbstract;
+import 
org.apache.causeway.core.metamodel.postprocessors.MetaModelPostProcessorAbstract;
 import org.apache.causeway.core.metamodel.progmodel.ProgrammingModel;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
@@ -50,7 +50,7 @@ import 
org.apache.causeway.extensions.secman.applib.user.dom.ApplicationUserRepo
 import lombok.val;
 
 public class TenantedAuthorizationPostProcessor
-extends ObjectSpecificationPostProcessorAbstract {
+extends MetaModelPostProcessorAbstract {
 
     @Component
     public static class Register implements MetaModelRefiner {
diff --git 
a/persistence/jdo/metamodel/src/main/java/org/apache/causeway/persistence/jdo/metamodel/facets/object/query/MetaModelVisitingValidatorForClauseAbstract.java
 
b/persistence/jdo/metamodel/src/main/java/org/apache/causeway/persistence/jdo/metamodel/facets/object/query/MetaModelVisitingValidatorForClauseAbstract.java
index b595850dc8..f7308e6506 100644
--- 
a/persistence/jdo/metamodel/src/main/java/org/apache/causeway/persistence/jdo/metamodel/facets/object/query/MetaModelVisitingValidatorForClauseAbstract.java
+++ 
b/persistence/jdo/metamodel/src/main/java/org/apache/causeway/persistence/jdo/metamodel/facets/object/query/MetaModelVisitingValidatorForClauseAbstract.java
@@ -25,14 +25,14 @@ import org.apache.causeway.commons.internal.base._Strings;
 import org.apache.causeway.commons.internal.context._Context;
 import org.apache.causeway.core.metamodel.context.MetaModelContext;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
-import 
org.apache.causeway.core.metamodel.specloader.validator.MetaModelVisitingValidatorAbstract;
+import 
org.apache.causeway.core.metamodel.specloader.validator.MetaModelValidatorAbstract;
 import 
org.apache.causeway.core.metamodel.specloader.validator.ValidationFailure;
 import 
org.apache.causeway.persistence.jdo.provider.metamodel.facets.object.query.JdoQueryFacet;
 
 import lombok.val;
 
 abstract class MetaModelVisitingValidatorForClauseAbstract
-extends MetaModelVisitingValidatorAbstract {
+extends MetaModelValidatorAbstract {
 
     final String clause;
 
@@ -44,7 +44,7 @@ extends MetaModelVisitingValidatorAbstract {
     }
 
     @Override
-    public void validate(final ObjectSpecification objectSpec) {
+    public void validateObjectEnter(final ObjectSpecification objectSpec) {
 
         if(objectSpec.isInjectable()) {
             return;
diff --git 
a/regressiontests/stable-domainmodel/src/test/java/org/apache/causeway/testdomain/domainmodel/DomainModelTest_usingBadDomain.java
 
b/regressiontests/stable-domainmodel/src/test/java/org/apache/causeway/testdomain/domainmodel/DomainModelTest_usingBadDomain.java
index 35fdf35293..3bb59a8e0c 100644
--- 
a/regressiontests/stable-domainmodel/src/test/java/org/apache/causeway/testdomain/domainmodel/DomainModelTest_usingBadDomain.java
+++ 
b/regressiontests/stable-domainmodel/src/test/java/org/apache/causeway/testdomain/domainmodel/DomainModelTest_usingBadDomain.java
@@ -363,6 +363,18 @@ class DomainModelTest_usingBadDomain {
 
     }
 
+    @Test
+    void memberIdClash() {
+
+        //TODO[CAUSEWAY-3051] WIP
+        System.err.println("------------------------------------------------");
+
+        
validator.streamFailures(id->id.getFullIdentityString().contains("InvalidMemberIdClash"))
+        .forEach(failure->System.err.printf("!!!%s%n", failure));
+
+        System.err.println("------------------------------------------------");
+    }
+
     // -- ELEMENT-TYPE
 
     @ParameterizedTest
diff --git 
a/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/model/bad/Configuration_usingInvalidDomain.java
 
b/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/model/bad/Configuration_usingInvalidDomain.java
index 6eb232ba78..aaafe8dcae 100644
--- 
a/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/model/bad/Configuration_usingInvalidDomain.java
+++ 
b/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/model/bad/Configuration_usingInvalidDomain.java
@@ -25,6 +25,7 @@ import org.springframework.context.annotation.Import;
 @Configuration
 @Import({
     InvalidDomainObjectOnInterface.class, // explicitly import interface
+    InvalidMemberIdClash.class,
 })
 @ComponentScan(
         basePackageClasses= {
diff --git 
a/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/model/bad/InvalidMemberIdClash.java
 
b/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/model/bad/InvalidMemberIdClash.java
new file mode 100644
index 0000000000..04b327636c
--- /dev/null
+++ 
b/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/model/bad/InvalidMemberIdClash.java
@@ -0,0 +1,208 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.testdomain.model.bad;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import javax.inject.Named;
+
+import org.apache.causeway.applib.annotation.Action;
+import org.apache.causeway.applib.annotation.ActionLayout;
+import org.apache.causeway.applib.annotation.Collection;
+import org.apache.causeway.applib.annotation.CollectionLayout;
+import org.apache.causeway.applib.annotation.DomainObject;
+import org.apache.causeway.applib.annotation.MemberSupport;
+import org.apache.causeway.applib.annotation.Nature;
+import org.apache.causeway.applib.annotation.Property;
+import org.apache.causeway.applib.annotation.PropertyLayout;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * Collisions between:
+ * <ul>
+ * <li>act<->act1,prop1,coll1</li>
+ * <li>prop<->act2,prop2,coll2</li>
+ * <li>coll<->act3,prop3,coll3</li>
+ * <li>act4<->prop4</li>
+ * <li>act5<->coll4</li>
+ * <li>prop5<->coll5</li>
+ * </ul>
+ */
+@DomainObject(nature = Nature.VIEW_MODEL)
+@Named("testdomain.InvalidMemberIdClash")
+public class InvalidMemberIdClash {
+
+    // member-id clash (act)
+    @Action
+    public boolean someAction() {
+        return false;
+    }
+
+    // member-id clash (prop)
+    @Property
+    @Getter
+    private int someProperty = 0;
+
+    // member-id clash (coll)
+    @Collection
+    @Getter
+    private List<Integer> someCollection = Collections.emptyList();
+
+    // -- MIXINS
+
+    @Action
+    @ActionLayout(named = "someAction") // member-id clash
+    @RequiredArgsConstructor
+    public static class ActionMixin1 {
+        @SuppressWarnings("unused")
+        private final InvalidMemberIdClash memberIdClash;
+        @MemberSupport public String act() { return ""; }
+    }
+
+    @Action
+    @ActionLayout(named = "someProperty") // member-id clash
+    @RequiredArgsConstructor
+    public static class ActionMixin2 {
+        @SuppressWarnings("unused")
+        private final InvalidMemberIdClash memberIdClash;
+        @MemberSupport public String act() { return ""; }
+    }
+
+    @Action
+    @ActionLayout(named = "someCollection") // member-id clash
+    @RequiredArgsConstructor
+    public static class ActionMixin3 {
+        @SuppressWarnings("unused")
+        private final InvalidMemberIdClash memberIdClash;
+        @MemberSupport public String act() { return ""; }
+    }
+
+    @Action
+    @ActionLayout(named = "mixinA") // member-id clash
+    @RequiredArgsConstructor
+    public static class ActionMixin4 {
+        @SuppressWarnings("unused")
+        private final InvalidMemberIdClash memberIdClash;
+        @MemberSupport public String act() { return ""; }
+    }
+
+    @Action
+    @ActionLayout(named = "mixinB") // member-id clash
+    @RequiredArgsConstructor
+    public static class ActionMixin5 {
+        @SuppressWarnings("unused")
+        private final InvalidMemberIdClash memberIdClash;
+        @MemberSupport public String act() { return ""; }
+    }
+
+    @Property
+    @PropertyLayout(named = "someAction") // member-id clash
+    @RequiredArgsConstructor
+    public static class PropertyMixin1 {
+        @SuppressWarnings("unused")
+        private final InvalidMemberIdClash memberIdClash;
+        @MemberSupport public String prop() { return ""; }
+    }
+
+    @Property
+    @PropertyLayout(named = "someProperty") // member-id clash
+    @RequiredArgsConstructor
+    public static class PropertyMixin2 {
+        @SuppressWarnings("unused")
+        private final InvalidMemberIdClash memberIdClash;
+        @MemberSupport public String prop() { return ""; }
+    }
+
+    @Property
+    @PropertyLayout(named = "someCollection") // member-id clash
+    @RequiredArgsConstructor
+    public static class PropertyMixin3 {
+        @SuppressWarnings("unused")
+        private final InvalidMemberIdClash memberIdClash;
+        @MemberSupport public String prop() { return ""; }
+    }
+
+    @Property
+    @PropertyLayout(named = "mixinA") // member-id clash
+    @RequiredArgsConstructor
+    public static class PropertyMixin4 {
+        @SuppressWarnings("unused")
+        private final InvalidMemberIdClash memberIdClash;
+        @MemberSupport public String prop() { return ""; }
+    }
+
+    @Property
+    @PropertyLayout(named = "mixinC") // member-id clash
+    @RequiredArgsConstructor
+    public static class PropertyMixin5 {
+        @SuppressWarnings("unused")
+        private final InvalidMemberIdClash memberIdClash;
+        @MemberSupport public String prop() { return ""; }
+    }
+
+    @Collection
+    @CollectionLayout(named = "someAction") // member-id clash
+    @RequiredArgsConstructor
+    public static class CollectionMixin1 {
+        @SuppressWarnings("unused")
+        private final InvalidMemberIdClash memberIdClash;
+        @MemberSupport public Set<String> coll() { return null; }
+    }
+
+    @Collection
+    @CollectionLayout(named = "someProperty") // member-id clash
+    @RequiredArgsConstructor
+    public static class CollectionMixin2 {
+        @SuppressWarnings("unused")
+        private final InvalidMemberIdClash memberIdClash;
+        @MemberSupport public Set<String> coll() { return null; }
+    }
+
+    @Collection
+    @CollectionLayout(named = "someCollection") // member-id clash
+    @RequiredArgsConstructor
+    public static class CollectionMixin3 {
+        @SuppressWarnings("unused")
+        private final InvalidMemberIdClash memberIdClash;
+        @MemberSupport public Set<String> coll() { return null; }
+    }
+
+    @Collection
+    @CollectionLayout(named = "mixinB") // member-id clash
+    @RequiredArgsConstructor
+    public static class CollectionMixin4 {
+        @SuppressWarnings("unused")
+        private final InvalidMemberIdClash memberIdClash;
+        @MemberSupport public Set<String> coll() { return null; }
+    }
+
+    @Collection
+    @CollectionLayout(named = "mixinC") // member-id clash
+    @RequiredArgsConstructor
+    public static class CollectionMixin5 {
+        @SuppressWarnings("unused")
+        private final InvalidMemberIdClash memberIdClash;
+        @MemberSupport public Set<String> coll() { return null; }
+    }
+
+}


Reply via email to