Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
kl3inIT marked this conversation as resolved.
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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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<KnowledgeAssetAuthorizationScope> findActiveAuthorizationScopes(
UUID organizationId,
Collection<UUID> knowledgeAssetIds) {
UUID tenantId = Objects.requireNonNull(organizationId, "organizationId");
List<UUID> ids = immutableIds(knowledgeAssetIds, "knowledgeAssetIds");
if (ids.isEmpty()) {
return List.of();
}
return List.copyOf(assets.findActiveAuthorizationScopes(
tenantId,
ids));
}

@Override
public List<KnowledgeCatalogItem> findCurrentCatalogItems(
UUID organizationId,
Collection<UUID> authorizedKnowledgeAssetIds) {
UUID tenantId = Objects.requireNonNull(organizationId, "organizationId");
List<UUID> ids = immutableIds(
authorizedKnowledgeAssetIds,
"authorizedKnowledgeAssetIds");
if (ids.isEmpty()) {
return List.of();
}
return List.copyOf(versions.findCurrentCatalogItems(
tenantId,
ids));
}

@Override
public Optional<KnowledgeCatalogItem> 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<KnowledgeCatalogItem> findCurrentCatalogItemByVersion(
UUID organizationId,
UUID knowledgeVersionId,
Collection<UUID> authorizedKnowledgeAssetIds) {
UUID tenantId = Objects.requireNonNull(organizationId, "organizationId");
UUID versionId = Objects.requireNonNull(knowledgeVersionId, "knowledgeVersionId");
List<UUID> ids = immutableIds(
authorizedKnowledgeAssetIds,
"authorizedKnowledgeAssetIds");
if (ids.isEmpty()) {
return Optional.empty();
}
return versions.findCurrentCatalogItemByVersion(
tenantId,
versionId,
ids);
}

private static List<UUID> immutableIds(
Collection<UUID> ids,
String name) {
return List.copyOf(Objects.requireNonNull(ids, name));
}
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>Implementations enforce organization ownership plus the active/current lifecycle predicates
* represented by each method.
*/
public interface KnowledgeAssetRetrievalQuery {

boolean exists(UUID organizationId, UUID knowledgeAssetId);

List<KnowledgeAssetAuthorizationScope> findActiveAuthorizationScopes(
UUID organizationId,
Collection<UUID> knowledgeAssetIds);

List<KnowledgeCatalogItem> findCurrentCatalogItems(
UUID organizationId,
Collection<UUID> authorizedKnowledgeAssetIds);

Optional<KnowledgeCatalogItem> findCurrentCatalogItem(
UUID organizationId,
UUID knowledgeAssetId,
UUID knowledgeVersionId);

Optional<KnowledgeCatalogItem> findCurrentCatalogItemByVersion(
UUID organizationId,
UUID knowledgeVersionId,
Collection<UUID> authorizedKnowledgeAssetIds);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
* <p>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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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)
Expand All @@ -37,7 +37,7 @@ public List<KnowledgeCatalogEntry> list(CurrentActor actor) {
if (scope.allAssetIds().isEmpty()) {
return List.of();
}
return versions.findCurrentCatalogItems(
return assets.findCurrentCatalogItems(
actor.organizationId(), scope.allAssetIds())
.stream()
.map(KnowledgeCatalogService::toEntry)
Expand All @@ -57,7 +57,7 @@ public Optional<KnowledgeCatalogEntry> findExactVisible(
if (!scope.allAssetIds().contains(knowledgeAssetId)) {
return Optional.empty();
}
return versions.findCurrentCatalogItem(
return assets.findCurrentCatalogItem(
actor.organizationId(), knowledgeAssetId, knowledgeVersionId)
.map(KnowledgeCatalogService::toEntry);
}
Expand All @@ -72,7 +72,7 @@ public Optional<KnowledgeCatalogEntry> findVersionVisible(
if (scope.allAssetIds().isEmpty()) {
return Optional.empty();
}
return versions.findCurrentCatalogItemByVersion(
return assets.findCurrentCatalogItemByVersion(
actor.organizationId(),
knowledgeVersionId,
scope.allAssetIds())
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -48,7 +48,7 @@ public class KnowledgeEvidenceScopeResolver {
KnowledgeEvidenceScopeResolver(
AppUserRepository users,
RelationshipAuthorizationSetPort authorization,
KnowledgeAssetRepository assets,
KnowledgeAssetRetrievalQuery assets,
SourceAclQuery aclQuery,
SecureKnowledgeRetrievalStore canonicalEvidence,
KnowledgeRetrievalProperties properties,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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();
Expand Down
Loading