From 9a760b837a2f1b4b97e65ae482d6d7f9539864d5 Mon Sep 17 00:00:00 2001 From: kl3inIT Date: Sun, 2 Aug 2026 01:29:58 +0700 Subject: [PATCH] refactor(knowledge): hide asset reads behind owner query --- ARCHITECTURE.md | 9 +- .../JpaKnowledgeAssetRetrievalQuery.java | 97 ++++++++++++++ .../asset/KnowledgeAssetRetrievalQuery.java | 35 +++++ .../core/knowledge/asset/package-info.java | 15 ++- .../AuthorizationResourceDirectory.java | 8 +- .../retrieval/KnowledgeCatalogService.java | 14 +- .../KnowledgeEvidenceScopeResolver.java | 6 +- .../knowledge/retrieval/package-info.java | 5 +- .../core/ModulithVerificationTests.java | 41 +++++- .../JpaKnowledgeAssetRetrievalQueryTests.java | 124 ++++++++++++++++++ ...nalPrincipalRetrievalIntegrationTests.java | 4 +- .../KnowledgeCatalogServiceTests.java | 22 ++-- .../KnowledgeEvidenceScopeResolverTests.java | 4 +- .../plan.md | 37 +++++- docs/specs/domains/asset-registry.md | 8 +- docs/specs/domains/secure-retrieval.md | 5 +- docs/tests/domains/asset-registry.md | 4 +- docs/tests/domains/secure-retrieval.md | 3 +- 18 files changed, 393 insertions(+), 48 deletions(-) create mode 100644 core/src/main/java/com/orgmemory/core/knowledge/asset/JpaKnowledgeAssetRetrievalQuery.java create mode 100644 core/src/main/java/com/orgmemory/core/knowledge/asset/KnowledgeAssetRetrievalQuery.java create mode 100644 core/src/test/java/com/orgmemory/core/knowledge/asset/JpaKnowledgeAssetRetrievalQueryTests.java diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ca6bbb5cc..f86850798 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -147,9 +147,12 @@ the parent-owned `knowledge::catalog` interface consumed by Asset Registry and the API; version-only reads resolve the canonical actor scope before querying a current active version. Asset also owns the compact embedding-profile reference required for publication and the projection namespace identity; callers -translate Retrieval's richer profile at the boundary. Asset has no direct -dependency on Retrieval and is a closed nested module with an exact outgoing -dependency allowlist. Parent Knowledge exposes the stable permission-aware +translate Retrieval's richer profile at the boundary. Retrieval resolves Asset +existence, active authorization scopes, and current catalog projections through +the Asset-owned `KnowledgeAssetRetrievalQuery`; it does not import Asset +repositories. Asset has no direct dependency on Retrieval and is a closed +nested module with an exact outgoing dependency allowlist. Parent Knowledge +exposes the stable permission-aware search contract, immutable evidence, secure result, and verified grounding as the exact `knowledge::search` named interface. Assistant and Asset Registry consume that parent interface without importing Retrieval implementation types. diff --git a/core/src/main/java/com/orgmemory/core/knowledge/asset/JpaKnowledgeAssetRetrievalQuery.java b/core/src/main/java/com/orgmemory/core/knowledge/asset/JpaKnowledgeAssetRetrievalQuery.java new file mode 100644 index 000000000..355868c2a --- /dev/null +++ b/core/src/main/java/com/orgmemory/core/knowledge/asset/JpaKnowledgeAssetRetrievalQuery.java @@ -0,0 +1,97 @@ +package com.orgmemory.core.knowledge.asset; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +class JpaKnowledgeAssetRetrievalQuery implements KnowledgeAssetRetrievalQuery { + + private final KnowledgeAssetRepository assets; + private final KnowledgeAssetVersionRepository versions; + + JpaKnowledgeAssetRetrievalQuery( + KnowledgeAssetRepository assets, + KnowledgeAssetVersionRepository versions) { + this.assets = assets; + this.versions = versions; + } + + @Override + public boolean exists(UUID organizationId, UUID knowledgeAssetId) { + return assets.existsByIdAndOrganizationId( + Objects.requireNonNull(knowledgeAssetId, "knowledgeAssetId"), + Objects.requireNonNull(organizationId, "organizationId")); + } + + @Override + public List findActiveAuthorizationScopes( + UUID organizationId, + Collection knowledgeAssetIds) { + UUID tenantId = Objects.requireNonNull(organizationId, "organizationId"); + List ids = immutableIds(knowledgeAssetIds, "knowledgeAssetIds"); + if (ids.isEmpty()) { + return List.of(); + } + return List.copyOf(assets.findActiveAuthorizationScopes( + tenantId, + ids)); + } + + @Override + public List findCurrentCatalogItems( + UUID organizationId, + Collection authorizedKnowledgeAssetIds) { + UUID tenantId = Objects.requireNonNull(organizationId, "organizationId"); + List ids = immutableIds( + authorizedKnowledgeAssetIds, + "authorizedKnowledgeAssetIds"); + if (ids.isEmpty()) { + return List.of(); + } + return List.copyOf(versions.findCurrentCatalogItems( + tenantId, + ids)); + } + + @Override + public Optional findCurrentCatalogItem( + UUID organizationId, + UUID knowledgeAssetId, + UUID knowledgeVersionId) { + return versions.findCurrentCatalogItem( + Objects.requireNonNull(organizationId, "organizationId"), + Objects.requireNonNull(knowledgeAssetId, "knowledgeAssetId"), + Objects.requireNonNull(knowledgeVersionId, "knowledgeVersionId")); + } + + @Override + public Optional findCurrentCatalogItemByVersion( + UUID organizationId, + UUID knowledgeVersionId, + Collection authorizedKnowledgeAssetIds) { + UUID tenantId = Objects.requireNonNull(organizationId, "organizationId"); + UUID versionId = Objects.requireNonNull(knowledgeVersionId, "knowledgeVersionId"); + List ids = immutableIds( + authorizedKnowledgeAssetIds, + "authorizedKnowledgeAssetIds"); + if (ids.isEmpty()) { + return Optional.empty(); + } + return versions.findCurrentCatalogItemByVersion( + tenantId, + versionId, + ids); + } + + private static List immutableIds( + Collection ids, + String name) { + return List.copyOf(Objects.requireNonNull(ids, name)); + } +} diff --git a/core/src/main/java/com/orgmemory/core/knowledge/asset/KnowledgeAssetRetrievalQuery.java b/core/src/main/java/com/orgmemory/core/knowledge/asset/KnowledgeAssetRetrievalQuery.java new file mode 100644 index 000000000..ca9c71f3d --- /dev/null +++ b/core/src/main/java/com/orgmemory/core/knowledge/asset/KnowledgeAssetRetrievalQuery.java @@ -0,0 +1,35 @@ +package com.orgmemory.core.knowledge.asset; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * Asset-owned read boundary for permission-aware retrieval and catalog federation. + * + *

Implementations enforce organization ownership plus the active/current lifecycle predicates + * represented by each method. + */ +public interface KnowledgeAssetRetrievalQuery { + + boolean exists(UUID organizationId, UUID knowledgeAssetId); + + List findActiveAuthorizationScopes( + UUID organizationId, + Collection knowledgeAssetIds); + + List findCurrentCatalogItems( + UUID organizationId, + Collection authorizedKnowledgeAssetIds); + + Optional findCurrentCatalogItem( + UUID organizationId, + UUID knowledgeAssetId, + UUID knowledgeVersionId); + + Optional findCurrentCatalogItemByVersion( + UUID organizationId, + UUID knowledgeVersionId, + Collection authorizedKnowledgeAssetIds); +} diff --git a/core/src/main/java/com/orgmemory/core/knowledge/asset/package-info.java b/core/src/main/java/com/orgmemory/core/knowledge/asset/package-info.java index 14b23b55d..7e07611e5 100644 --- a/core/src/main/java/com/orgmemory/core/knowledge/asset/package-info.java +++ b/core/src/main/java/com/orgmemory/core/knowledge/asset/package-info.java @@ -4,13 +4,14 @@ *

Graph consumers now resolve immutable asset, version, and chunk facts through an Asset-owned * query boundary. Promotion and source publication use Source Ledger-owned contracts rather than * its entities or repositories. Catalog projections, normalized text chunks, and pgvector - * encoding are Asset-owned persistence-facing values consumed by Retrieval. External catalog - * consumers cross the parent {@code knowledge::catalog} interface rather than this nested module. - * The compact embedding profile reference required for publication and projection namespace - * identity are also owned here; callers translate richer Retrieval profiles at the boundary. - * Asset has no direct dependency on Retrieval. The closed module exposes its owner-defined - * contracts from this root package and declares every outgoing application-module dependency - * explicitly. + * encoding are Asset-owned persistence-facing values. Retrieval reads Asset existence, + * authorization scopes, and current catalog projections through an Asset-owned query instead of + * importing its repositories. External catalog consumers cross the parent + * {@code knowledge::catalog} interface rather than this nested module. The compact embedding + * profile reference required for publication and projection namespace identity are also owned + * here; callers translate richer Retrieval profiles at the boundary. Asset has no direct + * dependency on Retrieval. The closed module exposes its owner-defined contracts from this root + * package and declares every outgoing application-module dependency explicitly. */ @org.springframework.modulith.ApplicationModule( type = org.springframework.modulith.ApplicationModule.Type.CLOSED, diff --git a/core/src/main/java/com/orgmemory/core/knowledge/retrieval/AuthorizationResourceDirectory.java b/core/src/main/java/com/orgmemory/core/knowledge/retrieval/AuthorizationResourceDirectory.java index 4a064a82c..2d6e05f50 100644 --- a/core/src/main/java/com/orgmemory/core/knowledge/retrieval/AuthorizationResourceDirectory.java +++ b/core/src/main/java/com/orgmemory/core/knowledge/retrieval/AuthorizationResourceDirectory.java @@ -2,7 +2,7 @@ import com.orgmemory.core.shared.error.KnowledgeResourceNotFoundException; -import com.orgmemory.core.knowledge.asset.KnowledgeAssetRepository; +import com.orgmemory.core.knowledge.asset.KnowledgeAssetRetrievalQuery; import com.orgmemory.core.authorization.ResourceRef; import com.orgmemory.core.knowledge.space.KnowledgeSpaceQuery; @@ -23,13 +23,13 @@ public class AuthorizationResourceDirectory { private final OrganizationRepository organizations; private final DepartmentRepository departments; private final KnowledgeSpaceQuery spaces; - private final KnowledgeAssetRepository assets; + private final KnowledgeAssetRetrievalQuery assets; AuthorizationResourceDirectory( OrganizationRepository organizations, DepartmentRepository departments, KnowledgeSpaceQuery spaces, - KnowledgeAssetRepository assets) { + KnowledgeAssetRetrievalQuery assets) { this.organizations = organizations; this.departments = departments; this.spaces = spaces; @@ -53,7 +53,7 @@ public ResourceRef require( case "knowledge_space" -> spaces.exists(organizationId, resourceId); case "knowledge_asset" -> - assets.existsByIdAndOrganizationId(resourceId, organizationId); + assets.exists(organizationId, resourceId); default -> false; }; if (!exists) { diff --git a/core/src/main/java/com/orgmemory/core/knowledge/retrieval/KnowledgeCatalogService.java b/core/src/main/java/com/orgmemory/core/knowledge/retrieval/KnowledgeCatalogService.java index 3546b2414..59fd2ea77 100644 --- a/core/src/main/java/com/orgmemory/core/knowledge/retrieval/KnowledgeCatalogService.java +++ b/core/src/main/java/com/orgmemory/core/knowledge/retrieval/KnowledgeCatalogService.java @@ -1,6 +1,6 @@ package com.orgmemory.core.knowledge.retrieval; -import com.orgmemory.core.knowledge.asset.KnowledgeAssetVersionRepository; +import com.orgmemory.core.knowledge.asset.KnowledgeAssetRetrievalQuery; import com.orgmemory.core.knowledge.asset.KnowledgeCatalogItem; import com.orgmemory.core.knowledge.catalog.KnowledgeCatalogEntry; import com.orgmemory.core.knowledge.catalog.KnowledgeCatalogQuery; @@ -20,13 +20,13 @@ public class KnowledgeCatalogService implements KnowledgeCatalogQuery { private final KnowledgeEvidenceScopeResolver evidenceScopes; - private final KnowledgeAssetVersionRepository versions; + private final KnowledgeAssetRetrievalQuery assets; KnowledgeCatalogService( KnowledgeEvidenceScopeResolver evidenceScopes, - KnowledgeAssetVersionRepository versions) { + KnowledgeAssetRetrievalQuery assets) { this.evidenceScopes = evidenceScopes; - this.versions = versions; + this.assets = assets; } @Transactional(readOnly = true) @@ -37,7 +37,7 @@ public List list(CurrentActor actor) { if (scope.allAssetIds().isEmpty()) { return List.of(); } - return versions.findCurrentCatalogItems( + return assets.findCurrentCatalogItems( actor.organizationId(), scope.allAssetIds()) .stream() .map(KnowledgeCatalogService::toEntry) @@ -57,7 +57,7 @@ public Optional findExactVisible( if (!scope.allAssetIds().contains(knowledgeAssetId)) { return Optional.empty(); } - return versions.findCurrentCatalogItem( + return assets.findCurrentCatalogItem( actor.organizationId(), knowledgeAssetId, knowledgeVersionId) .map(KnowledgeCatalogService::toEntry); } @@ -72,7 +72,7 @@ public Optional findVersionVisible( if (scope.allAssetIds().isEmpty()) { return Optional.empty(); } - return versions.findCurrentCatalogItemByVersion( + return assets.findCurrentCatalogItemByVersion( actor.organizationId(), knowledgeVersionId, scope.allAssetIds()) diff --git a/core/src/main/java/com/orgmemory/core/knowledge/retrieval/KnowledgeEvidenceScopeResolver.java b/core/src/main/java/com/orgmemory/core/knowledge/retrieval/KnowledgeEvidenceScopeResolver.java index 33b7b4ff6..66ce5c7c1 100644 --- a/core/src/main/java/com/orgmemory/core/knowledge/retrieval/KnowledgeEvidenceScopeResolver.java +++ b/core/src/main/java/com/orgmemory/core/knowledge/retrieval/KnowledgeEvidenceScopeResolver.java @@ -1,7 +1,7 @@ package com.orgmemory.core.knowledge.retrieval; import com.orgmemory.core.knowledge.asset.KnowledgeAssetAuthorizationScope; -import com.orgmemory.core.knowledge.asset.KnowledgeAssetRepository; +import com.orgmemory.core.knowledge.asset.KnowledgeAssetRetrievalQuery; import com.orgmemory.core.knowledge.acl.KnowledgeSpaceAclGenerationRef; import com.orgmemory.core.knowledge.acl.SourceAclQuery; import com.orgmemory.core.authorization.AuthorizedResourceQuery; @@ -39,7 +39,7 @@ public class KnowledgeEvidenceScopeResolver { private final AppUserRepository users; private final RelationshipAuthorizationSetPort authorization; - private final KnowledgeAssetRepository assets; + private final KnowledgeAssetRetrievalQuery assets; private final SourceAclQuery aclQuery; private final SecureKnowledgeRetrievalStore canonicalEvidence; private final KnowledgeRetrievalProperties properties; @@ -48,7 +48,7 @@ public class KnowledgeEvidenceScopeResolver { KnowledgeEvidenceScopeResolver( AppUserRepository users, RelationshipAuthorizationSetPort authorization, - KnowledgeAssetRepository assets, + KnowledgeAssetRetrievalQuery assets, SourceAclQuery aclQuery, SecureKnowledgeRetrievalStore canonicalEvidence, KnowledgeRetrievalProperties properties, diff --git a/core/src/main/java/com/orgmemory/core/knowledge/retrieval/package-info.java b/core/src/main/java/com/orgmemory/core/knowledge/retrieval/package-info.java index cbbc2982f..bbb2819eb 100644 --- a/core/src/main/java/com/orgmemory/core/knowledge/retrieval/package-info.java +++ b/core/src/main/java/com/orgmemory/core/knowledge/retrieval/package-info.java @@ -7,8 +7,9 @@ * Catalog, text-chunk, vector-literal, and projection-namespace values belong to Asset and are * consumed here one way. Top-level search consumers cross the parent-owned * {@code knowledge::search} interface instead of this implementation package. The module remains - * open while its sibling-module adapter surface and direct Asset persistence access are replaced by - * intentional interfaces during the Knowledge module-closing phase. + * open while its remaining sibling-module adapters and foreign Organization and Source Ledger + * persistence access are replaced by intentional interfaces during the Knowledge module-closing + * phase. Asset repository access already crosses an owner-defined query. */ @org.springframework.modulith.ApplicationModule( type = org.springframework.modulith.ApplicationModule.Type.OPEN) diff --git a/core/src/test/java/com/orgmemory/core/ModulithVerificationTests.java b/core/src/test/java/com/orgmemory/core/ModulithVerificationTests.java index 588fe9973..3add33185 100644 --- a/core/src/test/java/com/orgmemory/core/ModulithVerificationTests.java +++ b/core/src/test/java/com/orgmemory/core/ModulithVerificationTests.java @@ -792,9 +792,8 @@ void knowledgeAssetConsumerSurfaceDoesNotGainNewTypes() { "com.orgmemory.core.knowledge.asset.KnowledgeAssetGraphRef", "com.orgmemory.core.knowledge.asset.KnowledgeAssetPublicationService", "com.orgmemory.core.knowledge.asset.KnowledgeAssetRef", - "com.orgmemory.core.knowledge.asset.KnowledgeAssetRepository", + "com.orgmemory.core.knowledge.asset.KnowledgeAssetRetrievalQuery", "com.orgmemory.core.knowledge.asset.KnowledgeAssetVersionGraphRef", - "com.orgmemory.core.knowledge.asset.KnowledgeAssetVersionRepository", "com.orgmemory.core.knowledge.asset.KnowledgeCatalogItem", "com.orgmemory.core.knowledge.asset.KnowledgeChunkDraft", "com.orgmemory.core.knowledge.asset.KnowledgeEmbeddingProfileRef", @@ -804,6 +803,44 @@ void knowledgeAssetConsumerSurfaceDoesNotGainNewTypes() { consumedInternalTypes); } + @Test + void retrievalDoesNotDependOnAssetRepositories() { + var assetRepositoryTypes = Set.of( + "com.orgmemory.core.knowledge.asset.KnowledgeAssetRepository", + "com.orgmemory.core.knowledge.asset.KnowledgeAssetVersionRepository"); + var consumers = modules.stream() + .flatMap(module -> module.getDirectDependencies(modules).stream()) + .filter(dependency -> dependency.getSourceType() + .getPackageName() + .startsWith("com.orgmemory.core.knowledge.retrieval")) + .filter(dependency -> assetRepositoryTypes.contains( + dependency.getTargetType().getName())) + .map(dependency -> dependency.getSourceType().getName()) + .collect(TreeSet::new, Set::add, Set::addAll); + + assertEquals(Set.of(), consumers); + } + + @Test + void retrievalAssetReadsUseOnlyTheOwnerQuery() { + var consumers = modules.stream() + .flatMap(module -> module.getDirectDependencies(modules).stream()) + .filter(dependency -> dependency.getTargetType() + .getName() + .equals("com.orgmemory.core.knowledge.asset.KnowledgeAssetRetrievalQuery")) + .map(dependency -> dependency.getSourceType().getName()) + .filter(name -> name.startsWith( + "com.orgmemory.core.knowledge.retrieval")) + .collect(TreeSet::new, Set::add, Set::addAll); + + assertEquals( + Set.of( + "com.orgmemory.core.knowledge.retrieval.AuthorizationResourceDirectory", + "com.orgmemory.core.knowledge.retrieval.KnowledgeCatalogService", + "com.orgmemory.core.knowledge.retrieval.KnowledgeEvidenceScopeResolver"), + consumers); + } + @Test void objectStorageIsAnExplicitKnowledgeInterface() { var knowledge = modules.getModuleByName("knowledge").orElseThrow(); diff --git a/core/src/test/java/com/orgmemory/core/knowledge/asset/JpaKnowledgeAssetRetrievalQueryTests.java b/core/src/test/java/com/orgmemory/core/knowledge/asset/JpaKnowledgeAssetRetrievalQueryTests.java new file mode 100644 index 000000000..da331e5f0 --- /dev/null +++ b/core/src/test/java/com/orgmemory/core/knowledge/asset/JpaKnowledgeAssetRetrievalQueryTests.java @@ -0,0 +1,124 @@ +package com.orgmemory.core.knowledge.asset; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import com.orgmemory.core.permission.KnowledgeClassification; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import org.junit.jupiter.api.Test; + +class JpaKnowledgeAssetRetrievalQueryTests { + + private static final UUID ORGANIZATION_ID = UUID.randomUUID(); + private static final UUID SPACE_ID = UUID.randomUUID(); + private static final UUID ASSET_ID = UUID.randomUUID(); + private static final UUID VERSION_ID = UUID.randomUUID(); + + private final KnowledgeAssetRepository assets = mock(KnowledgeAssetRepository.class); + private final KnowledgeAssetVersionRepository versions = + mock(KnowledgeAssetVersionRepository.class); + private final JpaKnowledgeAssetRetrievalQuery query = + new JpaKnowledgeAssetRetrievalQuery(assets, versions); + + @Test + void existenceAndAuthorizationScopesKeepOrganizationAndLifecycleOwnershipInAsset() { + var scope = new KnowledgeAssetAuthorizationScope(ASSET_ID, SPACE_ID); + when(assets.existsByIdAndOrganizationId(ASSET_ID, ORGANIZATION_ID)) + .thenReturn(true); + when(assets.findActiveAuthorizationScopes( + ORGANIZATION_ID, + List.of(ASSET_ID))) + .thenReturn(List.of(scope)); + + assertTrue(query.exists(ORGANIZATION_ID, ASSET_ID)); + assertEquals( + List.of(scope), + query.findActiveAuthorizationScopes( + ORGANIZATION_ID, + List.of(ASSET_ID))); + } + + @Test + void catalogQueriesPreserveCurrentActiveVersionProjection() { + KnowledgeCatalogItem item = item(); + when(versions.findCurrentCatalogItems( + ORGANIZATION_ID, + List.of(ASSET_ID))) + .thenReturn(List.of(item)); + when(versions.findCurrentCatalogItem( + ORGANIZATION_ID, + ASSET_ID, + VERSION_ID)) + .thenReturn(Optional.of(item)); + when(versions.findCurrentCatalogItemByVersion( + ORGANIZATION_ID, + VERSION_ID, + List.of(ASSET_ID))) + .thenReturn(Optional.of(item)); + + assertEquals( + List.of(item), + query.findCurrentCatalogItems( + ORGANIZATION_ID, + List.of(ASSET_ID))); + assertEquals( + Optional.of(item), + query.findCurrentCatalogItem( + ORGANIZATION_ID, + ASSET_ID, + VERSION_ID)); + assertEquals( + Optional.of(item), + query.findCurrentCatalogItemByVersion( + ORGANIZATION_ID, + VERSION_ID, + List.of(ASSET_ID))); + } + + @Test + void emptyAuthorizedSetsNeverReachPersistence() { + assertEquals( + List.of(), + query.findActiveAuthorizationScopes( + ORGANIZATION_ID, + List.of())); + assertEquals( + List.of(), + query.findCurrentCatalogItems( + ORGANIZATION_ID, + List.of())); + assertFalse(query.findCurrentCatalogItemByVersion( + ORGANIZATION_ID, + VERSION_ID, + List.of()) + .isPresent()); + + verifyNoInteractions(assets, versions); + } + + @Test + void exactExistenceUsesAssetIdBeforeOrganizationIdAtPersistenceBoundary() { + query.exists(ORGANIZATION_ID, ASSET_ID); + + verify(assets).existsByIdAndOrganizationId(ASSET_ID, ORGANIZATION_ID); + } + + private static KnowledgeCatalogItem item() { + return new KnowledgeCatalogItem( + ASSET_ID, + VERSION_ID, + 3, + SPACE_ID, + "Support policy", + "en", + KnowledgeClassification.INTERNAL, + "a".repeat(64)); + } +} diff --git a/core/src/test/java/com/orgmemory/core/knowledge/retrieval/ExternalPrincipalRetrievalIntegrationTests.java b/core/src/test/java/com/orgmemory/core/knowledge/retrieval/ExternalPrincipalRetrievalIntegrationTests.java index 73269c176..1c01c2b07 100644 --- a/core/src/test/java/com/orgmemory/core/knowledge/retrieval/ExternalPrincipalRetrievalIntegrationTests.java +++ b/core/src/test/java/com/orgmemory/core/knowledge/retrieval/ExternalPrincipalRetrievalIntegrationTests.java @@ -1,7 +1,7 @@ package com.orgmemory.core.knowledge.retrieval; import com.orgmemory.core.knowledge.asset.KnowledgeAssetAuthorizationScope; -import com.orgmemory.core.knowledge.asset.KnowledgeAssetRepository; +import com.orgmemory.core.knowledge.asset.KnowledgeAssetRetrievalQuery; import com.orgmemory.core.knowledge.acl.KnowledgeSpaceAclGenerationRef; import com.orgmemory.core.knowledge.acl.SourceAclQuery; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -340,7 +340,7 @@ private static KnowledgeEvidenceScopeResolver resolverFor(UUID userId) { AppUserRepository users = mock(AppUserRepository.class); RelationshipAuthorizationSetPort authorization = mock(RelationshipAuthorizationSetPort.class); - KnowledgeAssetRepository assets = mock(KnowledgeAssetRepository.class); + KnowledgeAssetRetrievalQuery assets = mock(KnowledgeAssetRetrievalQuery.class); SourceAclQuery aclQuery = mock(SourceAclQuery.class); @SuppressWarnings("unchecked") ObjectProvider clocks = mock(ObjectProvider.class); diff --git a/core/src/test/java/com/orgmemory/core/knowledge/retrieval/KnowledgeCatalogServiceTests.java b/core/src/test/java/com/orgmemory/core/knowledge/retrieval/KnowledgeCatalogServiceTests.java index 15dccd10e..96e32a9bd 100644 --- a/core/src/test/java/com/orgmemory/core/knowledge/retrieval/KnowledgeCatalogServiceTests.java +++ b/core/src/test/java/com/orgmemory/core/knowledge/retrieval/KnowledgeCatalogServiceTests.java @@ -8,7 +8,7 @@ import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; -import com.orgmemory.core.knowledge.asset.KnowledgeAssetVersionRepository; +import com.orgmemory.core.knowledge.asset.KnowledgeAssetRetrievalQuery; import com.orgmemory.core.knowledge.asset.KnowledgeCatalogItem; import com.orgmemory.core.knowledge.catalog.KnowledgeCatalogEntry; import com.orgmemory.core.organization.CurrentActor; @@ -33,16 +33,16 @@ class KnowledgeCatalogServiceTests { private final KnowledgeEvidenceScopeResolver scopes = mock(KnowledgeEvidenceScopeResolver.class); - private final KnowledgeAssetVersionRepository versions = - mock(KnowledgeAssetVersionRepository.class); + private final KnowledgeAssetRetrievalQuery assets = + mock(KnowledgeAssetRetrievalQuery.class); private final KnowledgeCatalogService catalog = - new KnowledgeCatalogService(scopes, versions); + new KnowledgeCatalogService(scopes, assets); @Test void catalogUsesTheCanonicalPermissionResolvedAssetSet() { KnowledgeCatalogItem item = item(); when(scopes.resolve(ACTOR, null)).thenReturn(scope(Set.of(ASSET_ID))); - when(versions.findCurrentCatalogItems( + when(assets.findCurrentCatalogItems( ORGANIZATION_ID, Set.of(ASSET_ID))) .thenReturn(List.of(item)); @@ -56,7 +56,7 @@ void deniedExactVersionsReturnNoMetadataAndNeverReachTheRepository() { assertTrue(catalog.findExactVisible( ACTOR, ASSET_ID, VERSION_ID) .isEmpty()); - verifyNoInteractions(versions); + verifyNoInteractions(assets); } @Test @@ -65,14 +65,14 @@ void deniedVersionOnlyLookupsResolveAuthorizationBeforeTouchingPersistence() { assertTrue(catalog.findVersionVisible(ACTOR, VERSION_ID).isEmpty()); - verifyNoInteractions(versions); + verifyNoInteractions(assets); } @Test void onlyTheExactCurrentVersionCanBeFederated() { KnowledgeCatalogItem item = item(); when(scopes.resolve(ACTOR, null)).thenReturn(scope(Set.of(ASSET_ID))); - when(versions.findCurrentCatalogItem( + when(assets.findCurrentCatalogItem( ORGANIZATION_ID, ASSET_ID, VERSION_ID)) .thenReturn(Optional.of(item)); @@ -86,7 +86,7 @@ void onlyTheExactCurrentVersionCanBeFederated() { void versionOnlyLookupUsesTheAuthorizedAssetSetAndMapsEveryField() { KnowledgeCatalogItem item = item(); when(scopes.resolve(ACTOR, null)).thenReturn(scope(Set.of(ASSET_ID))); - when(versions.findCurrentCatalogItemByVersion( + when(assets.findCurrentCatalogItemByVersion( ORGANIZATION_ID, VERSION_ID, Set.of(ASSET_ID))) .thenReturn(Optional.of(item)); @@ -98,7 +98,7 @@ void versionOnlyLookupUsesTheAuthorizedAssetSetAndMapsEveryField() { @Test void authorizedMissingVersionAndDeniedVersionBothReturnOpaqueAbsence() { when(scopes.resolve(ACTOR, null)).thenReturn(scope(Set.of(ASSET_ID))); - when(versions.findCurrentCatalogItemByVersion( + when(assets.findCurrentCatalogItemByVersion( ORGANIZATION_ID, VERSION_ID, Set.of(ASSET_ID))) .thenReturn(Optional.empty()); @@ -115,7 +115,7 @@ void authorizationIndeterminacyPropagatesWithoutTouchingPersistence() { assertThrows( IllegalStateException.class, () -> catalog.findVersionVisible(ACTOR, VERSION_ID))); - verifyNoInteractions(versions); + verifyNoInteractions(assets); } private static ResolvedKnowledgeEvidenceScope scope(Set assetIds) { diff --git a/core/src/test/java/com/orgmemory/core/knowledge/retrieval/KnowledgeEvidenceScopeResolverTests.java b/core/src/test/java/com/orgmemory/core/knowledge/retrieval/KnowledgeEvidenceScopeResolverTests.java index 707a4d986..a82d556cd 100644 --- a/core/src/test/java/com/orgmemory/core/knowledge/retrieval/KnowledgeEvidenceScopeResolverTests.java +++ b/core/src/test/java/com/orgmemory/core/knowledge/retrieval/KnowledgeEvidenceScopeResolverTests.java @@ -1,7 +1,7 @@ package com.orgmemory.core.knowledge.retrieval; import com.orgmemory.core.knowledge.asset.KnowledgeAssetAuthorizationScope; -import com.orgmemory.core.knowledge.asset.KnowledgeAssetRepository; +import com.orgmemory.core.knowledge.asset.KnowledgeAssetRetrievalQuery; import com.orgmemory.core.knowledge.acl.KnowledgeSpaceAclGenerationRef; import com.orgmemory.core.knowledge.acl.SourceAclQuery; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -43,7 +43,7 @@ void administratorUsesOpenFgaAssetVisibilityWithoutImplicitExecutiveAccess() { AppUserRepository users = mock(AppUserRepository.class); RelationshipAuthorizationSetPort authorization = mock(RelationshipAuthorizationSetPort.class); - KnowledgeAssetRepository assets = mock(KnowledgeAssetRepository.class); + KnowledgeAssetRetrievalQuery assets = mock(KnowledgeAssetRetrievalQuery.class); SourceAclQuery aclQuery = mock(SourceAclQuery.class); SecureKnowledgeRetrievalStore canonical = mock(SecureKnowledgeRetrievalStore.class); diff --git a/docs/increments/active/2026-07-31-spring-modulith-package-refactor/plan.md b/docs/increments/active/2026-07-31-spring-modulith-package-refactor/plan.md index 40444b8d7..96355d574 100644 --- a/docs/increments/active/2026-07-31-spring-modulith-package-refactor/plan.md +++ b/docs/increments/active/2026-07-31-spring-modulith-package-refactor/plan.md @@ -1051,7 +1051,7 @@ passed. CodeRabbit remained rate limited; direct inspection confirmed zero inline comments, reviews, or review threads before merge, and both the PR head and merge commit are ancestors of current `origin/main`. -## Current Pull Request Gates +## Thirty-sixth Pull Request Evidence - Parent Knowledge exposes exactly `PermissionAwareKnowledgeSearch`, `RetrievedKnowledgeEvidence`, `SecureKnowledgeSearchResult`, and @@ -1089,3 +1089,38 @@ After merging current `origin/main` at `142a11cc`, the complete focused suite passed again in 39s. The documentation check passed across 473 Markdown files and 8 mirrored domain pairs, and all 40 release-policy tests passed again under Node 24.15. + +PR #250 merged as `ce1a970b` after Backend Java 25, Web Node 24, +documentation, evaluation, secret, impact, release-preview, release-policy, +and aggregate CI checks passed. CodeRabbit was rate limited; direct audit found +zero reviews, inline comments, or review threads, and both the PR head and merge +commit are ancestors of current `origin/main`. + +## Current Pull Request Gates + +- Asset owns `KnowledgeAssetRetrievalQuery` for tenant-scoped existence, active + authorization scopes, and current active catalog projections. +- Its JPA implementation remains package-private inside the closed Asset module; + Retrieval imports neither `KnowledgeAssetRepository` nor + `KnowledgeAssetVersionRepository`. +- `AuthorizationResourceDirectory`, `KnowledgeEvidenceScopeResolver`, and + `KnowledgeCatalogService` are the exact Retrieval consumers of the owner + query. Existing immutable Asset scope/catalog projections retain their shapes. +- Empty authorized sets return empty without persistence access; tenant, + archived-asset, current-version, and active-version predicates remain in the + Asset repositories behind the query. +- This code PR remains below 100 changed paths. Retrieval stays open for the + Organization, Source Ledger citation, Graph verifier, and adapter seams. + +Local verification so far: the repository-isolation test failed first against +the unchanged three repository consumers. Core/test/API/Worker compilation +passed in 39s. The Asset query, catalog, evidence-scope, and full Modulith test +slice passed in 37s. The real PostgreSQL external-principal scope proof and the +Spring API admin-resource integration proof passed together in 1m35s, confirming +the internal transactional bean and preserving authorization/resource behavior. +The combined full Core/API/Worker gate passed in 7m04s. The documentation +operating-model check passed across 478 Markdown files and 8 mirrored domain +pairs; all 40 release-policy tests passed under Node 24.15; and the terminating +repository-wide `clean test` passed in 1m03s across 99 tasks. Mechanical +repository-import, zero-byte, migration-scope, diff-hygiene, and 18-path scope +checks passed. diff --git a/docs/specs/domains/asset-registry.md b/docs/specs/domains/asset-registry.md index 15ea46e8b..dbaff71de 100644 --- a/docs/specs/domains/asset-registry.md +++ b/docs/specs/domains/asset-registry.md @@ -3,13 +3,14 @@ Source: `core/src/main/java/com/orgmemory/core/assetregistry`, `core/src/main/java/com/orgmemory/core/knowledge/catalog`, `core/src/main/java/com/orgmemory/core/knowledge/search`, +`core/src/main/java/com/orgmemory/core/knowledge/asset/KnowledgeAssetRetrievalQuery.java`, `core/src/main/java/com/orgmemory/core/knowledge/retrieval/KnowledgeCatalogService.java`, `apps/api/src/main/java/com/orgmemory/api/assetregistry`, `apps/api/src/main/java/com/orgmemory/api/knowledge`, `apps/mcp/src/main/java/com/orgmemory/mcp`, `apps/cli/src`, and `apps/web/src/features/assets`. -Reconciled: `2026-08-01-spring-modulith-package-refactor (13697ff9)`. +Reconciled: `2026-08-01-spring-modulith-package-refactor (ce1a970b)`. ## Current Behavior @@ -44,6 +45,11 @@ the canonical permission-aware retrieval path, and the run stores only citation identifiers plus a sanitized output digest by default. Raw sensitive variables and raw output are not retained. +Catalog federation and permission-scope resolution read Asset existence, +active authorization scopes, and current active version projections through an +Asset-owned query. Retrieval cannot import Asset repositories or bypass the +query's tenant and lifecycle predicates. + Evaluation executes only the bounded cases embedded in a release. Release comparison reports the two exact evaluation results; it does not change a release or promote a mutable alias. diff --git a/docs/specs/domains/secure-retrieval.md b/docs/specs/domains/secure-retrieval.md index 44732baca..8c3746edf 100644 --- a/docs/specs/domains/secure-retrieval.md +++ b/docs/specs/domains/secure-retrieval.md @@ -5,7 +5,7 @@ Source: `core/src/main/java/com/orgmemory/core/knowledge`, `apps/api/src/main/java/com/orgmemory/api/knowledge`, and `integrations/authorization-openfga`. -Reconciled: `2026-08-01-spring-modulith-package-refactor (13697ff9)`. +Reconciled: `2026-08-01-spring-modulith-package-refactor (ce1a970b)`. ## Current Behavior @@ -32,6 +32,9 @@ secure result, and verified grounding through the exact `knowledge::search` named interface. Assistant and Asset Registry cross that interface; the open Retrieval nested module retains the concrete engines, authorization sequence, ranking, and persistence while its remaining adapter seams are closed. +Asset existence, active authorization-scope, and current catalog reads cross +one Asset-owned query that keeps tenant and lifecycle predicates behind the +closed Asset module; Retrieval imports neither Asset repository. Citation URLs are opaque API routes, not object-storage URLs. Opening one reruns the current canonical evidence boundary once, validates the revision and blob diff --git a/docs/tests/domains/asset-registry.md b/docs/tests/domains/asset-registry.md index 8472d96c2..3758cbebe 100644 --- a/docs/tests/domains/asset-registry.md +++ b/docs/tests/domains/asset-registry.md @@ -1,6 +1,7 @@ # Asset Registry Coverage Source: `core/src/test/java/com/orgmemory/core/assetregistry`, +`core/src/test/java/com/orgmemory/core/knowledge/asset/JpaKnowledgeAssetRetrievalQueryTests.java`, `core/src/test/java/com/orgmemory/core/knowledge/retrieval/KnowledgeCatalogServiceTests.java`, `core/src/test/java/com/orgmemory/core/ModulithVerificationTests.java`, `apps/api/src/test/java/com/orgmemory/api/assetregistry`, @@ -8,7 +9,7 @@ Source: `core/src/test/java/com/orgmemory/core/assetregistry`, `apps/mcp/src/test/java/com/orgmemory/mcp`, `apps/cli/src/*.test.ts`, and `apps/web/src/features/assets/**/*.test.ts`. -Reconciled: `2026-08-01-spring-modulith-package-refactor (13697ff9)`. +Reconciled: `2026-08-01-spring-modulith-package-refactor (ce1a970b)`. | Behavior | Evidence | Status | | --- | --- | --- | @@ -53,6 +54,7 @@ Reconciled: `2026-08-01-spring-modulith-package-refactor (13697ff9)`. | Denied Pack component metadata and count remain opaque | `CapabilityPackServiceTests` | covered | | Knowledge catalog and Prompt grounding use canonical authorization | `KnowledgeCatalogServiceTests`, `PromptExecutionServiceTests` | covered | | Prompt grounding crosses only the parent `knowledge::search` contract and never imports Retrieval implementation | `ModulithVerificationTests#topLevelSearchConsumersUseOnlyTheParentSearchInterface`, `#assistantAndAssetRegistryDoNotDependOnRetrievalImplementation` | covered | +| Asset owns retrieval existence, active authorization-scope, and current catalog reads; Retrieval imports neither Asset repository | `JpaKnowledgeAssetRetrievalQueryTests`, `ModulithVerificationTests#retrievalDoesNotDependOnAssetRepositories`, `#retrievalAssetReadsUseOnlyTheOwnerQuery` | covered | | Version-only catalog lookup resolves authorization before persistence, keeps denied/missing results opaque, propagates indeterminacy, and maps every public field | `KnowledgeCatalogServiceTests` | covered | | Asset Registry catalog consumers cross only the exact parent `knowledge::catalog` surface and never import Asset internals | `ModulithVerificationTests#catalogIsAnExactExplicitKnowledgeInterface`, `ModulithVerificationTests#assetRegistryCatalogConsumersUseOnlyTheParentCatalogInterface`, `ModulithVerificationTests#assetRegistryDoesNotDependOnKnowledgeAssetInternals` | covered | | Catalog refactoring retains the committed `KnowledgeCatalogItem` OpenAPI component and wire contract | `OpenApiContractTests` | covered | diff --git a/docs/tests/domains/secure-retrieval.md b/docs/tests/domains/secure-retrieval.md index 371aa7b91..fee51acbc 100644 --- a/docs/tests/domains/secure-retrieval.md +++ b/docs/tests/domains/secure-retrieval.md @@ -5,7 +5,7 @@ Source: `core/src/test/java/com/orgmemory/core/knowledge`, `apps/api/src/test/java/com/orgmemory/api/knowledge`, and `integrations/authorization-openfga/src/test`. -Reconciled: `2026-08-01-spring-modulith-package-refactor (13697ff9)`. +Reconciled: `2026-08-01-spring-modulith-package-refactor (ce1a970b)`. Primary evidence: `apps/api/src/test/java/com/orgmemory/api/knowledge/KnowledgeRetrievalIntegrationTests.java` and `core/src/test/java/com/orgmemory/core/permission/KnowledgePermissionPolicyTests.java`. @@ -23,6 +23,7 @@ Primary evidence: `apps/api/src/test/java/com/orgmemory/api/knowledge/KnowledgeR | OpenFGA model mismatch cannot reach the renderer | `GraphRagKnowledgeRetrievalServiceTests#authorizationModelMismatchCannotReachTheVerifiedRenderer` | | Authorization scope changing during retrieval retries without egress | `GraphRagKnowledgeRetrievalServiceTests#revocationBetweenRetrievalAndCitationCausesAFullRetryWithoutEgress` | | Parent Knowledge exposes only the exact four-type `knowledge::search` contract, and Assistant plus Asset Registry do not import Retrieval implementation | `ModulithVerificationTests#searchIsAnExactExplicitKnowledgeInterface`, `#topLevelSearchConsumersUseOnlyTheParentSearchInterface`, `#assistantAndAssetRegistryDoNotDependOnRetrievalImplementation` | +| Retrieval crosses one Asset-owned query for existence, active authorization scopes, and current catalog projection without importing Asset repositories | `JpaKnowledgeAssetRetrievalQueryTests`, `ModulithVerificationTests#retrievalDoesNotDependOnAssetRepositories`, `#retrievalAssetReadsUseOnlyTheOwnerQuery`, `ExternalPrincipalRetrievalIntegrationTests`, `PermissionsAdminIntegrationTests` | Request-boundary missing control role/incomplete actor returns `403`; generic resource `404` does not claim otherwise. Provider-backed evaluation,