diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index f65732ce..6da4e9cb 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -164,8 +164,14 @@ 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.
-Retrieval remains explicitly open while its Graph verifier and remaining
-sibling adapters are replaced by intentional APIs. The
+Graph exploration, export, and curation obtain their immutable authorized
+evidence snapshot and exact current governing-evidence decision through the
+Retrieval-owned `GraphEvidenceVerifier`; Graph does not import Retrieval scope
+resolution, candidate, or store implementation types. Verified snapshots reject
+unknown Knowledge Spaces, and canonical evidence rechecks carry only the assets
+authorized for the requested Space. Retrieval remains
+explicitly open while its remaining sibling adapters are replaced by
+intentional APIs. The
provider-neutral object-storage port is exposed as the
`knowledge::storage` named interface. Leased database jobs carry ingestion work
across processes. A specific Knowledge Asset
diff --git a/core/src/main/java/com/orgmemory/core/knowledge/graph/GraphEvidenceScopeAccess.java b/core/src/main/java/com/orgmemory/core/knowledge/graph/GraphEvidenceScopeAccess.java
new file mode 100644
index 00000000..8e4b2ad6
--- /dev/null
+++ b/core/src/main/java/com/orgmemory/core/knowledge/graph/GraphEvidenceScopeAccess.java
@@ -0,0 +1,25 @@
+package com.orgmemory.core.knowledge.graph;
+
+import com.orgmemory.core.knowledge.retrieval.GraphEvidenceVerifier;
+import com.orgmemory.core.knowledge.retrieval.KnowledgeRetrievalUnavailableException;
+import com.orgmemory.core.knowledge.retrieval.VerifiedGraphEvidenceScope;
+import com.orgmemory.core.organization.CurrentActor;
+
+/** Shared Graph-side translation for unavailable canonical evidence scopes. */
+final class GraphEvidenceScopeAccess {
+
+ private GraphEvidenceScopeAccess() {}
+
+ static VerifiedGraphEvidenceScope verify(
+ GraphEvidenceVerifier verifier,
+ CurrentActor actor,
+ String authorizationModelId,
+ String unavailableMessage) {
+ try {
+ return verifier.verifyScope(actor, authorizationModelId);
+ } catch (KnowledgeRetrievalUnavailableException unavailable) {
+ throw new KnowledgeRetrievalUnavailableException(
+ unavailableMessage, unavailable);
+ }
+ }
+}
diff --git a/core/src/main/java/com/orgmemory/core/knowledge/graph/KnowledgeGraphCurationService.java b/core/src/main/java/com/orgmemory/core/knowledge/graph/KnowledgeGraphCurationService.java
index cf4b771f..99fd1e55 100644
--- a/core/src/main/java/com/orgmemory/core/knowledge/graph/KnowledgeGraphCurationService.java
+++ b/core/src/main/java/com/orgmemory/core/knowledge/graph/KnowledgeGraphCurationService.java
@@ -1,12 +1,10 @@
package com.orgmemory.core.knowledge.graph;
-import com.orgmemory.core.knowledge.retrieval.KnowledgeEvidenceScopeResolver;
-import com.orgmemory.core.knowledge.retrieval.KnowledgeEvidenceScopeUnavailableException;
+import com.orgmemory.core.knowledge.retrieval.GraphEvidenceVerifier;
import com.orgmemory.core.knowledge.asset.KnowledgeProjectionNamespaces;
import com.orgmemory.core.shared.error.KnowledgeResourceNotFoundException;
import com.orgmemory.core.knowledge.retrieval.KnowledgeRetrievalUnavailableException;
-import com.orgmemory.core.knowledge.retrieval.ResolvedKnowledgeEvidenceScope;
-import com.orgmemory.core.knowledge.retrieval.SecureKnowledgeRetrievalStore;
+import com.orgmemory.core.knowledge.retrieval.VerifiedGraphEvidenceScope;
import com.orgmemory.core.knowledge.asset.KnowledgeAssetGraphQuery;
import com.orgmemory.core.authorization.AuthorizationDecision;
@@ -45,8 +43,7 @@ public class KnowledgeGraphCurationService {
private final KnowledgeSpaceQuery spaces;
private final KnowledgeAssetGraphQuery assets;
private final RelationshipAuthorizationPort authorization;
- private final KnowledgeEvidenceScopeResolver evidenceScopes;
- private final SecureKnowledgeRetrievalStore canonicalEvidence;
+ private final GraphEvidenceVerifier evidenceVerifier;
private final GraphExportReader graphs;
private final GraphCurationStore curations;
private final ModelInvocationCache modelCache;
@@ -56,8 +53,7 @@ public class KnowledgeGraphCurationService {
KnowledgeSpaceQuery spaces,
KnowledgeAssetGraphQuery assets,
RelationshipAuthorizationPort authorization,
- KnowledgeEvidenceScopeResolver evidenceScopes,
- SecureKnowledgeRetrievalStore canonicalEvidence,
+ GraphEvidenceVerifier evidenceVerifier,
GraphExportReader graphs,
GraphCurationStore curations,
ModelInvocationCache modelCache,
@@ -65,8 +61,7 @@ public class KnowledgeGraphCurationService {
this.spaces = spaces;
this.assets = assets;
this.authorization = authorization;
- this.evidenceScopes = evidenceScopes;
- this.canonicalEvidence = canonicalEvidence;
+ this.evidenceVerifier = evidenceVerifier;
this.graphs = graphs;
this.curations = curations;
this.modelCache = modelCache;
@@ -83,7 +78,7 @@ public GraphCurationRecord apply(
actor, command.knowledgeSpaceId());
ProjectionNamespace namespace =
namespace(actor.organizationId(), command.knowledgeSpaceId());
- ResolvedKnowledgeEvidenceScope resolved =
+ VerifiedGraphEvidenceScope resolved =
resolve(actor, decision.policyVersion());
requireCurrentScope(command, resolved);
CurationProvenance provenance = new CurationProvenance(
@@ -198,11 +193,11 @@ public void deactivate(
requireSpace(actor, knowledgeSpaceId);
AuthorizationDecision decision =
requirePermission(actor, knowledgeSpaceId);
- ResolvedKnowledgeEvidenceScope resolved =
+ VerifiedGraphEvidenceScope resolved =
resolve(actor, decision.policyVersion());
- if (resolved.aclGenerationByKnowledgeSpace()
- .getOrDefault(knowledgeSpaceId, 0L)
- != authorizationGeneration) {
+ if (!resolved.includesKnowledgeSpace(knowledgeSpaceId)
+ || resolved.authorizationGeneration(knowledgeSpaceId)
+ != authorizationGeneration) {
throw new KnowledgeRetrievalUnavailableException(
"Knowledge graph authorization changed before curation");
}
@@ -229,30 +224,20 @@ private void requireGoverningEvidence(
CurrentActor actor,
UUID knowledgeSpaceId,
com.orgmemory.graphrag.model.EvidenceReference evidence,
- ResolvedKnowledgeEvidenceScope resolved) {
+ VerifiedGraphEvidenceScope resolved) {
if (!actor.organizationId().equals(evidence.organizationId())) {
throw new KnowledgeResourceNotFoundException();
}
assets.requireInSpace(
actor.organizationId(), evidence.knowledgeAssetId(), knowledgeSpaceId);
- var spaceScope = resolved.forKnowledgeSpace(knowledgeSpaceId);
- if (!spaceScope.includes(
+ if (!resolved.includes(
+ knowledgeSpaceId,
evidence.organizationId(), evidence.knowledgeAssetId())) {
throw new OrgMemoryAccessDeniedException(
"Governing evidence is not visible to the current actor");
}
- var candidates = canonicalEvidence.recheck(
- retrievalScope(resolved),
- java.util.List.of(Objects.requireNonNull(
- evidence.chunkId(), "governing evidence chunkId")));
- boolean current = candidates.size() == 1
- && candidates.getFirst().knowledgeAssetId()
- .equals(evidence.knowledgeAssetId())
- && candidates.getFirst().sourceRevisionId()
- .equals(evidence.sourceRevisionId())
- && candidates.getFirst().currentAclSnapshotId()
- .equals(evidence.aclSnapshotId());
- if (!current) {
+ if (!evidenceVerifier.isCurrentGoverningEvidence(
+ resolved, knowledgeSpaceId, evidence)) {
throw new OrgMemoryAccessDeniedException(
"Governing evidence is stale or unavailable");
}
@@ -260,11 +245,10 @@ private void requireGoverningEvidence(
private void requireCurrentScope(
KnowledgeGraphCurationCommand command,
- ResolvedKnowledgeEvidenceScope resolved) {
+ VerifiedGraphEvidenceScope resolved) {
UUID spaceId = command.knowledgeSpaceId();
- if (!resolved.knowledgeSpaceIds().contains(spaceId)
- || resolved.aclGenerationByKnowledgeSpace()
- .getOrDefault(spaceId, 0L)
+ if (!resolved.includesKnowledgeSpace(spaceId)
+ || resolved.authorizationGeneration(spaceId)
!= command.authorizationGeneration()) {
throw new KnowledgeRetrievalUnavailableException(
"Knowledge graph authorization changed before curation");
@@ -272,7 +256,7 @@ private void requireCurrentScope(
}
private void requireVisibleEntity(
- ResolvedKnowledgeEvidenceScope resolved,
+ VerifiedGraphEvidenceScope resolved,
ProjectionNamespace namespace,
UUID knowledgeSpaceId,
UUID entityId) {
@@ -285,7 +269,7 @@ private void requireVisibleEntity(
}
private void requireVisibleIdentity(
- ResolvedKnowledgeEvidenceScope resolved,
+ VerifiedGraphEvidenceScope resolved,
ProjectionNamespace namespace,
UUID knowledgeSpaceId,
com.orgmemory.graphrag.curation.GraphIdentityKind kind,
@@ -305,42 +289,29 @@ private void requireVisibleIdentity(
}
}
- private ResolvedKnowledgeEvidenceScope resolve(
+ private VerifiedGraphEvidenceScope resolve(
CurrentActor actor,
String authorizationModelId) {
- try {
- return evidenceScopes.resolve(actor, authorizationModelId);
- } catch (KnowledgeEvidenceScopeUnavailableException unavailable) {
- throw new KnowledgeRetrievalUnavailableException(
- "Knowledge graph permissions are temporarily unavailable");
- }
+ return GraphEvidenceScopeAccess.verify(
+ evidenceVerifier,
+ actor,
+ authorizationModelId,
+ "Knowledge graph permissions are temporarily unavailable");
}
private void requireUnchangedScope(
CurrentActor actor,
UUID knowledgeSpaceId,
String authorizationModelId,
- ResolvedKnowledgeEvidenceScope initial) {
- ResolvedKnowledgeEvidenceScope current =
+ VerifiedGraphEvidenceScope initial) {
+ VerifiedGraphEvidenceScope current =
resolve(actor, authorizationModelId);
- if (!initial.forKnowledgeSpace(knowledgeSpaceId)
- .authorizedAssetIds()
- .equals(current.forKnowledgeSpace(knowledgeSpaceId)
- .authorizedAssetIds())
- || initial.aclGenerationByKnowledgeSpace()
- .getOrDefault(knowledgeSpaceId, 0L)
- != current.aclGenerationByKnowledgeSpace()
- .getOrDefault(knowledgeSpaceId, 0L)) {
+ if (!initial.hasSameAssetsAndGeneration(current, knowledgeSpaceId)) {
throw new KnowledgeRetrievalUnavailableException(
"Knowledge graph authorization changed during curation");
}
}
- private static SecureKnowledgeRetrievalStore.RetrievalScope retrievalScope(
- ResolvedKnowledgeEvidenceScope scope) {
- return scope.toRetrievalScope();
- }
-
private void requireSpace(CurrentActor actor, UUID knowledgeSpaceId) {
if (!spaces.isActive(actor.organizationId(), knowledgeSpaceId)) {
throw new OrgMemoryAccessDeniedException(
diff --git a/core/src/main/java/com/orgmemory/core/knowledge/graph/KnowledgeGraphExplorerConfiguration.java b/core/src/main/java/com/orgmemory/core/knowledge/graph/KnowledgeGraphExplorerConfiguration.java
index 0eb5f6e5..7c23e829 100644
--- a/core/src/main/java/com/orgmemory/core/knowledge/graph/KnowledgeGraphExplorerConfiguration.java
+++ b/core/src/main/java/com/orgmemory/core/knowledge/graph/KnowledgeGraphExplorerConfiguration.java
@@ -1,6 +1,6 @@
package com.orgmemory.core.knowledge.graph;
-import com.orgmemory.core.knowledge.retrieval.KnowledgeEvidenceScopeResolver;
+import com.orgmemory.core.knowledge.retrieval.GraphEvidenceVerifier;
import com.orgmemory.core.authorization.RelationshipAuthorizationPort;
import com.orgmemory.core.knowledge.space.KnowledgeSpaceQuery;
import com.orgmemory.core.permission.PermissionAuditService;
@@ -17,14 +17,14 @@ public class KnowledgeGraphExplorerConfiguration {
KnowledgeGraphExplorerService knowledgeGraphExplorerService(
KnowledgeSpaceQuery spaces,
RelationshipAuthorizationPort authorization,
- KnowledgeEvidenceScopeResolver evidenceScopes,
+ GraphEvidenceVerifier evidenceVerifier,
GraphExportReader graphs,
GraphExplorerProperties properties,
PermissionAuditService audit) {
return new KnowledgeGraphExplorerService(
spaces,
authorization,
- evidenceScopes,
+ evidenceVerifier,
graphs,
properties,
audit);
diff --git a/core/src/main/java/com/orgmemory/core/knowledge/graph/KnowledgeGraphExplorerService.java b/core/src/main/java/com/orgmemory/core/knowledge/graph/KnowledgeGraphExplorerService.java
index 49425d56..16b58f75 100644
--- a/core/src/main/java/com/orgmemory/core/knowledge/graph/KnowledgeGraphExplorerService.java
+++ b/core/src/main/java/com/orgmemory/core/knowledge/graph/KnowledgeGraphExplorerService.java
@@ -1,10 +1,9 @@
package com.orgmemory.core.knowledge.graph;
-import com.orgmemory.core.knowledge.retrieval.KnowledgeEvidenceScopeResolver;
-import com.orgmemory.core.knowledge.retrieval.KnowledgeEvidenceScopeUnavailableException;
import com.orgmemory.core.knowledge.asset.KnowledgeProjectionNamespaces;
+import com.orgmemory.core.knowledge.retrieval.GraphEvidenceVerifier;
import com.orgmemory.core.knowledge.retrieval.KnowledgeRetrievalUnavailableException;
-import com.orgmemory.core.knowledge.retrieval.ResolvedKnowledgeEvidenceScope;
+import com.orgmemory.core.knowledge.retrieval.VerifiedGraphEvidenceScope;
import com.orgmemory.core.authorization.AuthorizationDecision;
import com.orgmemory.core.authorization.PermissionKey;
import com.orgmemory.core.authorization.RelationshipAuthorizationPort;
@@ -41,7 +40,7 @@ public class KnowledgeGraphExplorerService {
private final KnowledgeSpaceQuery spaces;
private final RelationshipAuthorizationPort authorization;
- private final KnowledgeEvidenceScopeResolver evidenceScopes;
+ private final GraphEvidenceVerifier evidenceVerifier;
private final GraphExportReader graphs;
private final GraphExplorerProperties properties;
private final PermissionAuditService audit;
@@ -49,13 +48,13 @@ public class KnowledgeGraphExplorerService {
public KnowledgeGraphExplorerService(
KnowledgeSpaceQuery spaces,
RelationshipAuthorizationPort authorization,
- KnowledgeEvidenceScopeResolver evidenceScopes,
+ GraphEvidenceVerifier evidenceVerifier,
GraphExportReader graphs,
GraphExplorerProperties properties,
PermissionAuditService audit) {
this.spaces = spaces;
this.authorization = authorization;
- this.evidenceScopes = evidenceScopes;
+ this.evidenceVerifier = evidenceVerifier;
this.graphs = graphs;
this.properties = properties;
this.audit = audit;
@@ -98,9 +97,9 @@ private KnowledgeGraphView explore(
String requestId,
String policyVersion,
int attempt) {
- ResolvedKnowledgeEvidenceScope initial =
+ VerifiedGraphEvidenceScope initial =
resolve(actor, policyVersion);
- if (!initial.knowledgeSpaceIds().contains(knowledgeSpaceId)) {
+ if (!initial.includesKnowledgeSpace(knowledgeSpaceId)) {
return empty(
actor,
knowledgeSpaceId,
@@ -114,9 +113,9 @@ private KnowledgeGraphView explore(
initial.forKnowledgeSpace(knowledgeSpaceId),
namespace);
- ResolvedKnowledgeEvidenceScope current =
+ VerifiedGraphEvidenceScope current =
resolve(actor, policyVersion);
- if (!sameSpaceScope(initial, current, knowledgeSpaceId)) {
+ if (!initial.hasSameSpaceScope(current, knowledgeSpaceId)) {
if (attempt == 0) {
return explore(
actor,
@@ -154,8 +153,7 @@ private KnowledgeGraphView explore(
entityLimit,
properties.maximumRelationLimit(),
maximumDepth,
- initial.aclGenerationByKnowledgeSpace()
- .getOrDefault(knowledgeSpaceId, 0L),
+ initial.authorizationGeneration(knowledgeSpaceId),
curationDecision.allowed());
audit.record(new PermissionAuditCommand(
actor.organizationId(),
@@ -191,15 +189,14 @@ private String requireSpaceAccess(
return decision.policyVersion();
}
- private ResolvedKnowledgeEvidenceScope resolve(
+ private VerifiedGraphEvidenceScope resolve(
CurrentActor actor,
String policyVersion) {
- try {
- return evidenceScopes.resolve(actor, policyVersion);
- } catch (KnowledgeEvidenceScopeUnavailableException unavailable) {
- throw new KnowledgeRetrievalUnavailableException(
- "Knowledge graph permissions are temporarily unavailable");
- }
+ return GraphEvidenceScopeAccess.verify(
+ evidenceVerifier,
+ actor,
+ policyVersion,
+ "Knowledge graph permissions are temporarily unavailable");
}
private KnowledgeGraphView empty(
@@ -406,18 +403,6 @@ private static boolean contains(String value, String needle) {
return value.toLowerCase(Locale.ROOT).contains(needle);
}
- private static boolean sameSpaceScope(
- ResolvedKnowledgeEvidenceScope initial,
- ResolvedKnowledgeEvidenceScope current,
- UUID knowledgeSpaceId) {
- return initial.authorizationModelId()
- .equals(current.authorizationModelId())
- && initial.forKnowledgeSpace(knowledgeSpaceId)
- .authorizationFingerprint()
- .equals(current.forKnowledgeSpace(knowledgeSpaceId)
- .authorizationFingerprint());
- }
-
private String normalizeQuery(String query) {
if (query == null || query.isBlank()) {
return "";
diff --git a/core/src/main/java/com/orgmemory/core/knowledge/graph/KnowledgeGraphExportService.java b/core/src/main/java/com/orgmemory/core/knowledge/graph/KnowledgeGraphExportService.java
index eb340ecb..ab17d632 100644
--- a/core/src/main/java/com/orgmemory/core/knowledge/graph/KnowledgeGraphExportService.java
+++ b/core/src/main/java/com/orgmemory/core/knowledge/graph/KnowledgeGraphExportService.java
@@ -1,10 +1,9 @@
package com.orgmemory.core.knowledge.graph;
-import com.orgmemory.core.knowledge.retrieval.KnowledgeEvidenceScopeResolver;
-import com.orgmemory.core.knowledge.retrieval.KnowledgeEvidenceScopeUnavailableException;
+import com.orgmemory.core.knowledge.retrieval.GraphEvidenceVerifier;
import com.orgmemory.core.knowledge.asset.KnowledgeProjectionNamespaces;
import com.orgmemory.core.knowledge.retrieval.KnowledgeRetrievalUnavailableException;
-import com.orgmemory.core.knowledge.retrieval.ResolvedKnowledgeEvidenceScope;
+import com.orgmemory.core.knowledge.retrieval.VerifiedGraphEvidenceScope;
import com.orgmemory.core.authorization.PermissionKey;
import com.orgmemory.core.authorization.RelationshipAuthorizationPort;
import com.orgmemory.core.authorization.RelationshipAuthorizationQuery;
@@ -35,7 +34,7 @@ public class KnowledgeGraphExportService {
private final KnowledgeSpaceQuery spaces;
private final RelationshipAuthorizationPort authorization;
- private final KnowledgeEvidenceScopeResolver evidenceScopes;
+ private final GraphEvidenceVerifier evidenceVerifier;
private final GraphExportReader reader;
private final GraphExportFormatter formatter = new GraphExportFormatter();
private final PermissionAuditService audit;
@@ -43,12 +42,12 @@ public class KnowledgeGraphExportService {
KnowledgeGraphExportService(
KnowledgeSpaceQuery spaces,
RelationshipAuthorizationPort authorization,
- KnowledgeEvidenceScopeResolver evidenceScopes,
+ GraphEvidenceVerifier evidenceVerifier,
GraphExportReader reader,
PermissionAuditService audit) {
this.spaces = spaces;
this.authorization = authorization;
- this.evidenceScopes = evidenceScopes;
+ this.evidenceVerifier = evidenceVerifier;
this.reader = reader;
this.audit = audit;
}
@@ -75,27 +74,19 @@ public GraphExportFormatter.Artifact export(
if (!entry.allowed()) {
throw accessDenied();
}
- ResolvedKnowledgeEvidenceScope resolved;
- try {
- resolved = evidenceScopes.resolve(actor, entry.policyVersion());
- } catch (KnowledgeEvidenceScopeUnavailableException unavailable) {
- throw new KnowledgeRetrievalUnavailableException(
- "Knowledge graph permissions changed while preparing the export",
- unavailable);
+ VerifiedGraphEvidenceScope resolved =
+ resolve(actor, entry.policyVersion());
+ if (!resolved.includesKnowledgeSpace(knowledgeSpaceId)) {
+ throw accessDenied();
}
ProjectionNamespace namespace = KnowledgeProjectionNamespaces.forSpace(
actor.organizationId(), knowledgeSpaceId);
var document = reader.read(
resolved.forKnowledgeSpace(knowledgeSpaceId),
namespace);
- ResolvedKnowledgeEvidenceScope current;
- try {
- current = evidenceScopes.resolve(actor, entry.policyVersion());
- } catch (KnowledgeEvidenceScopeUnavailableException unavailable) {
- throw new KnowledgeRetrievalUnavailableException(
- "Knowledge graph permissions changed while preparing the export");
- }
- if (!sameSpaceScope(resolved, current, knowledgeSpaceId)) {
+ VerifiedGraphEvidenceScope current =
+ resolve(actor, entry.policyVersion());
+ if (!resolved.hasSameSpaceScope(current, knowledgeSpaceId)) {
throw new KnowledgeRetrievalUnavailableException(
"Knowledge graph permissions changed while preparing the export");
}
@@ -115,19 +106,14 @@ public GraphExportFormatter.Artifact export(
return artifact;
}
- private static boolean sameSpaceScope(
- ResolvedKnowledgeEvidenceScope first,
- ResolvedKnowledgeEvidenceScope second,
- UUID knowledgeSpaceId) {
- return first.authorizationModelId().equals(second.authorizationModelId())
- && first.forKnowledgeSpace(knowledgeSpaceId)
- .authorizedAssetIds()
- .equals(second.forKnowledgeSpace(knowledgeSpaceId)
- .authorizedAssetIds())
- && first.aclGenerationByKnowledgeSpace()
- .getOrDefault(knowledgeSpaceId, 0L)
- .equals(second.aclGenerationByKnowledgeSpace()
- .getOrDefault(knowledgeSpaceId, 0L));
+ private VerifiedGraphEvidenceScope resolve(
+ CurrentActor actor,
+ String authorizationModelId) {
+ return GraphEvidenceScopeAccess.verify(
+ evidenceVerifier,
+ actor,
+ authorizationModelId,
+ "Knowledge graph permissions changed while preparing the export");
}
private static OrgMemoryAccessDeniedException accessDenied() {
diff --git a/core/src/main/java/com/orgmemory/core/knowledge/graph/package-info.java b/core/src/main/java/com/orgmemory/core/knowledge/graph/package-info.java
index 693c362d..f00e010b 100644
--- a/core/src/main/java/com/orgmemory/core/knowledge/graph/package-info.java
+++ b/core/src/main/java/com/orgmemory/core/knowledge/graph/package-info.java
@@ -1,9 +1,10 @@
/**
* Knowledge graph indexing, processing profiles, exploration, curation, and export.
*
- *
Asset, Source Ledger, ACL, Space, and embedding-profile state crosses owned query or registry
- * boundaries instead of persistence types. The closed boundary exposes only root-package Graph
- * contracts and declares every outgoing application-module dependency explicitly.
+ *
Asset, Source Ledger, ACL, Space, Retrieval evidence, and embedding-profile state crosses
+ * owned query, verifier, or registry boundaries instead of persistence and implementation types.
+ * The closed boundary exposes only root-package Graph contracts 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/CanonicalGraphEvidenceVerifier.java b/core/src/main/java/com/orgmemory/core/knowledge/retrieval/CanonicalGraphEvidenceVerifier.java
new file mode 100644
index 00000000..923a6270
--- /dev/null
+++ b/core/src/main/java/com/orgmemory/core/knowledge/retrieval/CanonicalGraphEvidenceVerifier.java
@@ -0,0 +1,79 @@
+package com.orgmemory.core.knowledge.retrieval;
+
+import com.orgmemory.core.organization.CurrentActor;
+import com.orgmemory.graphrag.model.EvidenceReference;
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@Transactional(readOnly = true)
+class CanonicalGraphEvidenceVerifier implements GraphEvidenceVerifier {
+
+ private final KnowledgeEvidenceScopeResolver evidenceScopes;
+ private final SecureKnowledgeRetrievalStore canonicalEvidence;
+
+ CanonicalGraphEvidenceVerifier(
+ KnowledgeEvidenceScopeResolver evidenceScopes,
+ SecureKnowledgeRetrievalStore canonicalEvidence) {
+ this.evidenceScopes = evidenceScopes;
+ this.canonicalEvidence = canonicalEvidence;
+ }
+
+ @Override
+ public VerifiedGraphEvidenceScope verifyScope(
+ CurrentActor actor,
+ String expectedAuthorizationModelId) {
+ try {
+ ResolvedKnowledgeEvidenceScope resolved = evidenceScopes.resolve(
+ Objects.requireNonNull(actor, "actor"),
+ expectedAuthorizationModelId);
+ return new VerifiedGraphEvidenceScope(
+ resolved.organizationId(),
+ resolved.actorUserId(),
+ resolved.actorDepartmentId(),
+ resolved.actorExecutive(),
+ resolved.authorizationModelId(),
+ resolved.evaluatedAt(),
+ resolved.assetIdsByKnowledgeSpace(),
+ resolved.aclGenerationByKnowledgeSpace());
+ } catch (KnowledgeEvidenceScopeUnavailableException unavailable) {
+ throw new KnowledgeRetrievalUnavailableException(
+ "Canonical Graph evidence scope is unavailable",
+ unavailable);
+ }
+ }
+
+ @Override
+ public boolean isCurrentGoverningEvidence(
+ VerifiedGraphEvidenceScope scope,
+ UUID knowledgeSpaceId,
+ EvidenceReference evidence) {
+ Objects.requireNonNull(scope, "scope");
+ Objects.requireNonNull(knowledgeSpaceId, "knowledgeSpaceId");
+ Objects.requireNonNull(evidence, "evidence");
+ if (!scope.includes(
+ knowledgeSpaceId,
+ evidence.organizationId(),
+ evidence.knowledgeAssetId())) {
+ return false;
+ }
+ var candidates = canonicalEvidence.recheck(
+ scope.toRetrievalScope(knowledgeSpaceId),
+ List.of(Objects.requireNonNull(
+ evidence.chunkId(), "governing evidence chunkId")));
+ return candidates.size() == 1
+ && candidates.getFirst().organizationId()
+ .equals(evidence.organizationId())
+ && candidates.getFirst().knowledgeAssetId()
+ .equals(evidence.knowledgeAssetId())
+ && candidates.getFirst().sourceRevisionId()
+ .equals(evidence.sourceRevisionId())
+ && candidates.getFirst().currentAclSnapshotId()
+ .equals(evidence.aclSnapshotId())
+ && candidates.getFirst().chunkId()
+ .equals(evidence.chunkId());
+ }
+}
diff --git a/core/src/main/java/com/orgmemory/core/knowledge/retrieval/GraphEvidenceVerifier.java b/core/src/main/java/com/orgmemory/core/knowledge/retrieval/GraphEvidenceVerifier.java
new file mode 100644
index 00000000..437d1c54
--- /dev/null
+++ b/core/src/main/java/com/orgmemory/core/knowledge/retrieval/GraphEvidenceVerifier.java
@@ -0,0 +1,18 @@
+package com.orgmemory.core.knowledge.retrieval;
+
+import com.orgmemory.core.organization.CurrentActor;
+import com.orgmemory.graphrag.model.EvidenceReference;
+import java.util.UUID;
+
+/** Retrieval-owned canonical authorization boundary for Graph evidence reads. */
+public interface GraphEvidenceVerifier {
+
+ VerifiedGraphEvidenceScope verifyScope(
+ CurrentActor actor,
+ String expectedAuthorizationModelId);
+
+ boolean isCurrentGoverningEvidence(
+ VerifiedGraphEvidenceScope scope,
+ UUID knowledgeSpaceId,
+ EvidenceReference evidence);
+}
diff --git a/core/src/main/java/com/orgmemory/core/knowledge/retrieval/VerifiedGraphEvidenceScope.java b/core/src/main/java/com/orgmemory/core/knowledge/retrieval/VerifiedGraphEvidenceScope.java
new file mode 100644
index 00000000..f97e8373
--- /dev/null
+++ b/core/src/main/java/com/orgmemory/core/knowledge/retrieval/VerifiedGraphEvidenceScope.java
@@ -0,0 +1,156 @@
+package com.orgmemory.core.knowledge.retrieval;
+
+import com.orgmemory.graphrag.authorization.AuthorizedEvidenceScope;
+import java.time.Instant;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.UUID;
+
+/** Immutable permission-verified evidence snapshot exposed to Graph use cases. */
+public record VerifiedGraphEvidenceScope(
+ UUID organizationId,
+ UUID actorUserId,
+ UUID actorDepartmentId,
+ boolean actorExecutive,
+ String authorizationModelId,
+ Instant evaluatedAt,
+ Map> assetIdsByKnowledgeSpace,
+ Map aclGenerationByKnowledgeSpace) {
+
+ public VerifiedGraphEvidenceScope {
+ Objects.requireNonNull(organizationId, "organizationId");
+ Objects.requireNonNull(actorUserId, "actorUserId");
+ authorizationModelId = required(
+ authorizationModelId, "authorizationModelId");
+ Objects.requireNonNull(evaluatedAt, "evaluatedAt");
+ assetIdsByKnowledgeSpace = immutableSets(assetIdsByKnowledgeSpace);
+ aclGenerationByKnowledgeSpace = Map.copyOf(Objects.requireNonNull(
+ aclGenerationByKnowledgeSpace,
+ "aclGenerationByKnowledgeSpace"));
+ if (!aclGenerationByKnowledgeSpace.keySet()
+ .equals(assetIdsByKnowledgeSpace.keySet())) {
+ throw new IllegalArgumentException(
+ "ACL generations and Knowledge Space scopes must align");
+ }
+ if (aclGenerationByKnowledgeSpace.values().stream()
+ .anyMatch(generation -> generation == null || generation < 0)) {
+ throw new IllegalArgumentException(
+ "ACL generations must be non-negative");
+ }
+ }
+
+ public boolean includesKnowledgeSpace(UUID knowledgeSpaceId) {
+ return assetIdsByKnowledgeSpace.containsKey(
+ Objects.requireNonNull(knowledgeSpaceId, "knowledgeSpaceId"));
+ }
+
+ public boolean includes(
+ UUID knowledgeSpaceId,
+ UUID candidateOrganizationId,
+ UUID candidateAssetId) {
+ UUID spaceId = Objects.requireNonNull(
+ knowledgeSpaceId, "knowledgeSpaceId");
+ return organizationId.equals(candidateOrganizationId)
+ && assetIdsByKnowledgeSpace
+ .getOrDefault(spaceId, Set.of())
+ .contains(candidateAssetId);
+ }
+
+ public AuthorizedEvidenceScope forKnowledgeSpace(UUID knowledgeSpaceId) {
+ UUID spaceId = requireKnowledgeSpace(knowledgeSpaceId);
+ return new AuthorizedEvidenceScope(
+ organizationId,
+ actorUserId,
+ actorDepartmentId,
+ actorExecutive,
+ assetIdsByKnowledgeSpace.get(spaceId),
+ authorizationModelId,
+ authorizationGeneration(spaceId),
+ evaluatedAt);
+ }
+
+ public long authorizationGeneration(UUID knowledgeSpaceId) {
+ return aclGenerationByKnowledgeSpace.get(
+ requireKnowledgeSpace(knowledgeSpaceId));
+ }
+
+ public boolean hasSameAuthorizationFingerprint(
+ VerifiedGraphEvidenceScope other,
+ UUID knowledgeSpaceId) {
+ Objects.requireNonNull(other, "other");
+ return includesKnowledgeSpace(knowledgeSpaceId)
+ && other.includesKnowledgeSpace(knowledgeSpaceId)
+ && forKnowledgeSpace(knowledgeSpaceId)
+ .authorizationFingerprint()
+ .equals(other.forKnowledgeSpace(knowledgeSpaceId)
+ .authorizationFingerprint());
+ }
+
+ public boolean hasSameAssetsAndGeneration(
+ VerifiedGraphEvidenceScope other,
+ UUID knowledgeSpaceId) {
+ Objects.requireNonNull(other, "other");
+ return includesKnowledgeSpace(knowledgeSpaceId)
+ && other.includesKnowledgeSpace(knowledgeSpaceId)
+ && forKnowledgeSpace(knowledgeSpaceId)
+ .authorizedAssetIds()
+ .equals(other.forKnowledgeSpace(knowledgeSpaceId)
+ .authorizedAssetIds())
+ && authorizationGeneration(knowledgeSpaceId)
+ == other.authorizationGeneration(knowledgeSpaceId);
+ }
+
+ public boolean hasSameSpaceScope(
+ VerifiedGraphEvidenceScope other,
+ UUID knowledgeSpaceId) {
+ Objects.requireNonNull(other, "other");
+ return authorizationModelId.equals(other.authorizationModelId())
+ && hasSameAssetsAndGeneration(other, knowledgeSpaceId);
+ }
+
+ SecureKnowledgeRetrievalStore.RetrievalScope toRetrievalScope(
+ UUID knowledgeSpaceId) {
+ UUID spaceId = requireKnowledgeSpace(knowledgeSpaceId);
+ return new SecureKnowledgeRetrievalStore.RetrievalScope(
+ organizationId,
+ actorUserId,
+ actorDepartmentId,
+ actorExecutive,
+ assetIdsByKnowledgeSpace.get(spaceId).stream()
+ .sorted()
+ .toList(),
+ authorizationModelId,
+ evaluatedAt);
+ }
+
+ private UUID requireKnowledgeSpace(UUID knowledgeSpaceId) {
+ UUID spaceId = Objects.requireNonNull(
+ knowledgeSpaceId, "knowledgeSpaceId");
+ if (!assetIdsByKnowledgeSpace.containsKey(spaceId)) {
+ throw new IllegalArgumentException(
+ "Knowledge Space is not part of the verified scope");
+ }
+ return spaceId;
+ }
+
+ private static Map> immutableSets(
+ Map> source) {
+ Map> copy = new LinkedHashMap<>();
+ Objects.requireNonNull(source, "assetIdsByKnowledgeSpace")
+ .forEach((space, assets) -> copy.put(
+ Objects.requireNonNull(space, "knowledgeSpaceId"),
+ Set.copyOf(Objects.requireNonNull(
+ assets, "assetIds"))));
+ return Map.copyOf(copy);
+ }
+
+ private static String required(String value, String field) {
+ String normalized = value == null ? "" : value.strip();
+ if (normalized.isEmpty()) {
+ throw new IllegalArgumentException(field + " is required");
+ }
+ return normalized;
+ }
+}
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 99b3b5aa..94f81cd4 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
@@ -6,10 +6,12 @@
* Graph indexing now resolves profiles through the registry instead of profile persistence.
* 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 Graph verifier and remaining sibling-module adapters are replaced by intentional
- * interfaces during the Knowledge module-closing phase. Asset, Organization, and Source Ledger
- * citation reads already cross owner-defined queries.
+ * {@code knowledge::search} interface instead of this implementation package. Graph exploration,
+ * export, and curation consume a Retrieval-owned canonical evidence verifier and immutable verified
+ * snapshot instead of scope resolution or retrieval-store implementation types. The module remains
+ * open while its remaining sibling-module adapters are replaced by intentional interfaces during
+ * the Knowledge module-closing phase. Asset, Organization, and Source Ledger citation reads already
+ * cross owner-defined queries.
*/
@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 6b2311d6..910107f5 100644
--- a/core/src/test/java/com/orgmemory/core/ModulithVerificationTests.java
+++ b/core/src/test/java/com/orgmemory/core/ModulithVerificationTests.java
@@ -400,12 +400,9 @@ void graphConsumesOnlyRetrievalGraphContracts() {
Set.of(
"com.orgmemory.core.knowledge.retrieval.EmbeddingProfileRef",
"com.orgmemory.core.knowledge.retrieval.EmbeddingProfileRegistry",
- "com.orgmemory.core.knowledge.retrieval.KnowledgeEvidenceScopeResolver",
+ "com.orgmemory.core.knowledge.retrieval.GraphEvidenceVerifier",
"com.orgmemory.core.knowledge.retrieval.KnowledgeRetrievalUnavailableException",
- "com.orgmemory.core.knowledge.retrieval.ResolvedKnowledgeEvidenceScope",
- "com.orgmemory.core.knowledge.retrieval.SecureKnowledgeRetrievalStore",
- "com.orgmemory.core.knowledge.retrieval.SecureKnowledgeRetrievalStore$RetrievalScope",
- "com.orgmemory.core.knowledge.retrieval.SecureRetrievalCandidate"),
+ "com.orgmemory.core.knowledge.retrieval.VerifiedGraphEvidenceScope"),
consumedTypes);
}
@@ -719,6 +716,7 @@ void knowledgeRetrievalTemporaryOpenBoundaryDoesNotGainNewConsumers() {
"com.orgmemory.core.knowledge.connector.ConnectorReconciler",
"com.orgmemory.core.knowledge.connector.ConnectorSourceRevisionCoordinator",
"com.orgmemory.core.knowledge.graph.ClaimedGraphIndex",
+ "com.orgmemory.core.knowledge.graph.GraphEvidenceScopeAccess",
"com.orgmemory.core.knowledge.graph.GraphIndexingCoordinator",
"com.orgmemory.core.knowledge.graph.KnowledgeGraphCurationService",
"com.orgmemory.core.knowledge.graph.KnowledgeGraphExplorerConfiguration",
@@ -729,12 +727,9 @@ void knowledgeRetrievalTemporaryOpenBoundaryDoesNotGainNewConsumers() {
Set.of(
"com.orgmemory.core.knowledge.retrieval.EmbeddingProfileRef",
"com.orgmemory.core.knowledge.retrieval.EmbeddingProfileRegistry",
- "com.orgmemory.core.knowledge.retrieval.KnowledgeEvidenceScopeResolver",
+ "com.orgmemory.core.knowledge.retrieval.GraphEvidenceVerifier",
"com.orgmemory.core.knowledge.retrieval.KnowledgeRetrievalUnavailableException",
- "com.orgmemory.core.knowledge.retrieval.ResolvedKnowledgeEvidenceScope",
- "com.orgmemory.core.knowledge.retrieval.SecureKnowledgeRetrievalStore",
- "com.orgmemory.core.knowledge.retrieval.SecureKnowledgeRetrievalStore$RetrievalScope",
- "com.orgmemory.core.knowledge.retrieval.SecureRetrievalCandidate"),
+ "com.orgmemory.core.knowledge.retrieval.VerifiedGraphEvidenceScope"),
consumedInternalTypes);
}
diff --git a/core/src/test/java/com/orgmemory/core/knowledge/graph/KnowledgeGraphCurationServiceTests.java b/core/src/test/java/com/orgmemory/core/knowledge/graph/KnowledgeGraphCurationServiceTests.java
index 3272ebb5..ff66a665 100644
--- a/core/src/test/java/com/orgmemory/core/knowledge/graph/KnowledgeGraphCurationServiceTests.java
+++ b/core/src/test/java/com/orgmemory/core/knowledge/graph/KnowledgeGraphCurationServiceTests.java
@@ -1,10 +1,9 @@
package com.orgmemory.core.knowledge.graph;
-import com.orgmemory.core.knowledge.retrieval.KnowledgeEvidenceScopeResolver;
+import com.orgmemory.core.knowledge.retrieval.GraphEvidenceVerifier;
+import com.orgmemory.core.knowledge.retrieval.KnowledgeRetrievalUnavailableException;
import com.orgmemory.core.shared.error.KnowledgeResourceNotFoundException;
-import com.orgmemory.core.knowledge.retrieval.ResolvedKnowledgeEvidenceScope;
-import com.orgmemory.core.knowledge.retrieval.SecureKnowledgeRetrievalStore;
-import com.orgmemory.core.knowledge.retrieval.SecureRetrievalCandidate;
+import com.orgmemory.core.knowledge.retrieval.VerifiedGraphEvidenceScope;
import com.orgmemory.core.knowledge.asset.KnowledgeAssetGraphQuery;
import com.orgmemory.core.knowledge.space.KnowledgeSpaceQuery;
@@ -28,7 +27,6 @@
import com.orgmemory.graphrag.export.GraphExportReader;
import com.orgmemory.graphrag.model.EvidenceReference;
import java.time.Instant;
-import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
@@ -51,10 +49,8 @@ class KnowledgeGraphCurationServiceTests {
mock(KnowledgeAssetGraphQuery.class);
private final RelationshipAuthorizationPort authorization =
mock(RelationshipAuthorizationPort.class);
- private final KnowledgeEvidenceScopeResolver evidenceScopes =
- mock(KnowledgeEvidenceScopeResolver.class);
- private final SecureKnowledgeRetrievalStore canonicalEvidence =
- mock(SecureKnowledgeRetrievalStore.class);
+ private final GraphEvidenceVerifier evidenceVerifier =
+ mock(GraphEvidenceVerifier.class);
private final GraphExportReader graphs = mock(GraphExportReader.class);
private final GraphCurationStore store = mock(GraphCurationStore.class);
private final ModelInvocationCache modelCache =
@@ -66,8 +62,7 @@ class KnowledgeGraphCurationServiceTests {
spaces,
assets,
authorization,
- evidenceScopes,
- canonicalEvidence,
+ evidenceVerifier,
graphs,
store,
modelCache,
@@ -80,8 +75,8 @@ void setUpSpaceAndEvidence() {
when(spaces.isActive(ORGANIZATION_ID, SPACE_ID))
.thenReturn(true);
when(store.append(any(), any())).thenAnswer(invocation -> invocation.getArgument(1));
- when(evidenceScopes.resolve(actor, "model-v1"))
- .thenReturn(new ResolvedKnowledgeEvidenceScope(
+ when(evidenceVerifier.verifyScope(actor, "model-v1"))
+ .thenReturn(new VerifiedGraphEvidenceScope(
ORGANIZATION_ID,
USER_ID,
null,
@@ -90,25 +85,8 @@ void setUpSpaceAndEvidence() {
Instant.parse("2026-07-24T00:00:00Z"),
Map.of(SPACE_ID, Set.of(ASSET_ID)),
Map.of(SPACE_ID, 7L)));
- when(canonicalEvidence.recheck(any(), any()))
- .thenReturn(List.of(new SecureRetrievalCandidate(
- ORGANIZATION_ID,
- CHUNK_ID,
- ASSET_ID,
- UUID.randomUUID(),
- REVISION_ID,
- "Policy",
- "Approved policy",
- "source://policy",
- null,
- null,
- null,
- 0,
- ACL_ID,
- ACL_ID,
- "model-v1",
- UUID.randomUUID(),
- 1)));
+ when(evidenceVerifier.isCurrentGoverningEvidence(any(), any(), any()))
+ .thenReturn(true);
}
@Test
@@ -184,6 +162,59 @@ void governingEvidenceCannotCrossKnowledgeSpaces() {
verify(store, never()).append(any(), any());
}
+ @Test
+ void staleGoverningEvidenceFailsClosedBeforeTheLedger() {
+ when(authorization.check(any()))
+ .thenReturn(AuthorizationDecision.allow("model-v1"));
+ when(evidenceVerifier.isCurrentGoverningEvidence(any(), any(), any()))
+ .thenReturn(false);
+
+ OrgMemoryAccessDeniedException thrown = assertThrows(
+ OrgMemoryAccessDeniedException.class,
+ () -> service.apply(
+ actor,
+ new KnowledgeGraphCurationCommand.CurateEntity(
+ SPACE_ID,
+ "curation-1",
+ "attempt",
+ 7,
+ ENTITY_ID,
+ "Policy",
+ "POLICY",
+ "Stale",
+ evidence())));
+
+ assertEquals("Governing evidence is stale or unavailable", thrown.getMessage());
+ verify(store, never()).append(any(), any());
+ }
+
+ @Test
+ void deactivateFailsClosedWhenTheVerifiedScopeDoesNotIncludeTheSpace() {
+ when(authorization.check(any()))
+ .thenReturn(AuthorizationDecision.allow("model-v1"));
+ when(evidenceVerifier.verifyScope(actor, "model-v1"))
+ .thenReturn(new VerifiedGraphEvidenceScope(
+ ORGANIZATION_ID,
+ USER_ID,
+ null,
+ false,
+ "model-v1",
+ Instant.parse("2026-07-24T00:00:00Z"),
+ Map.of(),
+ Map.of()));
+
+ assertThrows(
+ KnowledgeRetrievalUnavailableException.class,
+ () -> service.deactivate(
+ actor,
+ SPACE_ID,
+ UUID.randomUUID(),
+ 0L,
+ "withdraw"));
+
+ verify(store, never()).deactivate(any(), any(), any());
+ }
+
private static EvidenceReference evidence() {
return new EvidenceReference(
ORGANIZATION_ID,
diff --git a/core/src/test/java/com/orgmemory/core/knowledge/graph/KnowledgeGraphExplorerServiceTests.java b/core/src/test/java/com/orgmemory/core/knowledge/graph/KnowledgeGraphExplorerServiceTests.java
index b457e55d..527d30e8 100644
--- a/core/src/test/java/com/orgmemory/core/knowledge/graph/KnowledgeGraphExplorerServiceTests.java
+++ b/core/src/test/java/com/orgmemory/core/knowledge/graph/KnowledgeGraphExplorerServiceTests.java
@@ -1,8 +1,8 @@
package com.orgmemory.core.knowledge.graph;
-import com.orgmemory.core.knowledge.retrieval.KnowledgeEvidenceScopeResolver;
+import com.orgmemory.core.knowledge.retrieval.GraphEvidenceVerifier;
import com.orgmemory.core.knowledge.retrieval.KnowledgeRetrievalUnavailableException;
-import com.orgmemory.core.knowledge.retrieval.ResolvedKnowledgeEvidenceScope;
+import com.orgmemory.core.knowledge.retrieval.VerifiedGraphEvidenceScope;
import com.orgmemory.core.knowledge.space.KnowledgeSpaceQuery;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -68,8 +68,8 @@ class KnowledgeGraphExplorerServiceTests {
private final KnowledgeSpaceQuery spaces = mock(KnowledgeSpaceQuery.class);
private final RelationshipAuthorizationPort authorization =
mock(RelationshipAuthorizationPort.class);
- private final KnowledgeEvidenceScopeResolver evidenceScopes =
- mock(KnowledgeEvidenceScopeResolver.class);
+ private final GraphEvidenceVerifier evidenceVerifier =
+ mock(GraphEvidenceVerifier.class);
private final GraphExportReader reader = mock(GraphExportReader.class);
private final PermissionAuditService audit =
mock(PermissionAuditService.class);
@@ -86,7 +86,7 @@ class KnowledgeGraphExplorerServiceTests {
new KnowledgeGraphExplorerService(
spaces,
authorization,
- evidenceScopes,
+ evidenceVerifier,
reader,
properties,
audit);
@@ -97,7 +97,7 @@ void setUpEntryPermission() {
.thenReturn(true);
when(authorization.check(any()))
.thenReturn(AuthorizationDecision.allow("model-v1"));
- when(evidenceScopes.resolve(actor, "model-v1"))
+ when(evidenceVerifier.verifyScope(actor, "model-v1"))
.thenReturn(scope(Set.of(ASSET_ID), 9L));
when(reader.read(any(), any())).thenReturn(document());
}
@@ -212,7 +212,7 @@ void deniesBeforeReadingWhenTheSpaceIsNotAuthorized() {
@Test
void failsClosedWhenAuthorizationKeepsChangingDuringRead() {
- when(evidenceScopes.resolve(actor, "model-v1"))
+ when(evidenceVerifier.verifyScope(actor, "model-v1"))
.thenReturn(
scope(Set.of(ASSET_ID), 9L),
scope(Set.of(ASSET_ID), 10L),
@@ -228,10 +228,10 @@ void failsClosedWhenAuthorizationKeepsChangingDuringRead() {
verify(audit, never()).record(any());
}
- private static ResolvedKnowledgeEvidenceScope scope(
+ private static VerifiedGraphEvidenceScope scope(
Set assetIds,
long generation) {
- return new ResolvedKnowledgeEvidenceScope(
+ return new VerifiedGraphEvidenceScope(
ORGANIZATION_ID,
USER_ID,
null,
diff --git a/core/src/test/java/com/orgmemory/core/knowledge/graph/KnowledgeGraphExportServiceTests.java b/core/src/test/java/com/orgmemory/core/knowledge/graph/KnowledgeGraphExportServiceTests.java
index 044b6b69..f88a7eb0 100644
--- a/core/src/test/java/com/orgmemory/core/knowledge/graph/KnowledgeGraphExportServiceTests.java
+++ b/core/src/test/java/com/orgmemory/core/knowledge/graph/KnowledgeGraphExportServiceTests.java
@@ -1,9 +1,8 @@
package com.orgmemory.core.knowledge.graph;
-import com.orgmemory.core.knowledge.retrieval.KnowledgeEvidenceScopeResolver;
-import com.orgmemory.core.knowledge.retrieval.KnowledgeEvidenceScopeUnavailableException;
+import com.orgmemory.core.knowledge.retrieval.GraphEvidenceVerifier;
import com.orgmemory.core.knowledge.retrieval.KnowledgeRetrievalUnavailableException;
-import com.orgmemory.core.knowledge.retrieval.ResolvedKnowledgeEvidenceScope;
+import com.orgmemory.core.knowledge.retrieval.VerifiedGraphEvidenceScope;
import com.orgmemory.core.knowledge.space.KnowledgeSpaceQuery;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -16,6 +15,7 @@
import com.orgmemory.core.authorization.AuthorizationDecision;
import com.orgmemory.core.authorization.RelationshipAuthorizationPort;
import com.orgmemory.core.organization.CurrentActor;
+import com.orgmemory.core.organization.OrgMemoryAccessDeniedException;
import com.orgmemory.core.permission.PermissionAuditService;
import com.orgmemory.graphrag.authorization.AuthorizedEvidenceScope;
import com.orgmemory.graphrag.export.GraphExportDocument;
@@ -44,8 +44,8 @@ class KnowledgeGraphExportServiceTests {
private final KnowledgeSpaceQuery spaces = mock(KnowledgeSpaceQuery.class);
private final RelationshipAuthorizationPort authorization =
mock(RelationshipAuthorizationPort.class);
- private final KnowledgeEvidenceScopeResolver evidenceScopes =
- mock(KnowledgeEvidenceScopeResolver.class);
+ private final GraphEvidenceVerifier evidenceVerifier =
+ mock(GraphEvidenceVerifier.class);
private final GraphExportReader reader = mock(GraphExportReader.class);
private final PermissionAuditService audit =
mock(PermissionAuditService.class);
@@ -55,7 +55,7 @@ class KnowledgeGraphExportServiceTests {
new KnowledgeGraphExportService(
spaces,
authorization,
- evidenceScopes,
+ evidenceVerifier,
reader,
audit);
@@ -65,8 +65,8 @@ void setUpEntryPermission() {
.thenReturn(true);
when(authorization.check(any()))
.thenReturn(AuthorizationDecision.allow("model-v1"));
- when(evidenceScopes.resolve(actor, "model-v1")).thenReturn(
- new ResolvedKnowledgeEvidenceScope(
+ when(evidenceVerifier.verifyScope(actor, "model-v1")).thenReturn(
+ new VerifiedGraphEvidenceScope(
ORGANIZATION_ID,
USER_ID,
null,
@@ -101,10 +101,9 @@ void exportsOnlyTheCurrentAuthorizedEvidenceScopeAndAuditsEgress() {
@Test
void reportsUnavailableForUnexpectedOpenFgaObjectTypesBeforeReadingGraphData() {
- when(evidenceScopes.resolve(actor, "model-v1")).thenThrow(
- new KnowledgeEvidenceScopeUnavailableException(
- "AUTHORIZED_OBJECT_SET_INVALID",
- "model-v1"));
+ when(evidenceVerifier.verifyScope(actor, "model-v1")).thenThrow(
+ new KnowledgeRetrievalUnavailableException(
+ "Canonical Graph evidence scope is unavailable"));
assertThrows(
KnowledgeRetrievalUnavailableException.class,
@@ -115,4 +114,26 @@ void reportsUnavailableForUnexpectedOpenFgaObjectTypesBeforeReadingGraphData() {
verify(audit, never()).record(any());
}
+ @Test
+ void deniesBeforeReadingWhenTheVerifiedScopeDoesNotIncludeTheSpace() {
+ when(evidenceVerifier.verifyScope(actor, "model-v1")).thenReturn(
+ new VerifiedGraphEvidenceScope(
+ ORGANIZATION_ID,
+ USER_ID,
+ null,
+ false,
+ "model-v1",
+ Instant.parse("2026-07-24T00:00:00Z"),
+ Map.of(),
+ Map.of()));
+
+ assertThrows(
+ OrgMemoryAccessDeniedException.class,
+ () -> service.export(
+ actor, SPACE_ID, GraphExportFormat.JSON, "request-1"));
+
+ verify(reader, never()).read(any(), any());
+ verify(audit, never()).record(any());
+ }
+
}
diff --git a/core/src/test/java/com/orgmemory/core/knowledge/retrieval/CanonicalGraphEvidenceVerifierTests.java b/core/src/test/java/com/orgmemory/core/knowledge/retrieval/CanonicalGraphEvidenceVerifierTests.java
new file mode 100644
index 00000000..3f9b0eb6
--- /dev/null
+++ b/core/src/test/java/com/orgmemory/core/knowledge/retrieval/CanonicalGraphEvidenceVerifierTests.java
@@ -0,0 +1,304 @@
+package com.orgmemory.core.knowledge.retrieval;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.orgmemory.core.organization.CurrentActor;
+import com.orgmemory.graphrag.model.EvidenceReference;
+import java.time.Instant;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+class CanonicalGraphEvidenceVerifierTests {
+
+ private static final UUID ORGANIZATION_ID = UUID.randomUUID();
+ private static final UUID USER_ID = UUID.randomUUID();
+ private static final UUID SPACE_ID = UUID.randomUUID();
+ private static final UUID ASSET_ID = UUID.randomUUID();
+ private static final UUID SOURCE_OBJECT_ID = UUID.randomUUID();
+ private static final UUID REVISION_ID = UUID.randomUUID();
+ private static final UUID ACL_ID = UUID.randomUUID();
+ private static final UUID CHUNK_ID = UUID.randomUUID();
+ private static final UUID PROFILE_ID = UUID.randomUUID();
+ private static final Instant EVALUATED_AT =
+ Instant.parse("2026-08-02T00:00:00Z");
+
+ private final KnowledgeEvidenceScopeResolver evidenceScopes =
+ mock(KnowledgeEvidenceScopeResolver.class);
+ private final SecureKnowledgeRetrievalStore canonicalEvidence =
+ mock(SecureKnowledgeRetrievalStore.class);
+ private final CanonicalGraphEvidenceVerifier verifier =
+ new CanonicalGraphEvidenceVerifier(evidenceScopes, canonicalEvidence);
+ private final CurrentActor actor =
+ new CurrentActor(USER_ID, ORGANIZATION_ID, null, "User", "user@example.com");
+
+ @BeforeEach
+ void setUpScope() {
+ when(evidenceScopes.resolve(actor, "model-v1"))
+ .thenReturn(resolvedScope());
+ }
+
+ @Test
+ void exposesAnImmutableGraphSnapshotWithoutLeakingTheInternalScope() {
+ VerifiedGraphEvidenceScope result =
+ verifier.verifyScope(actor, "model-v1");
+
+ assertEquals(ORGANIZATION_ID, result.organizationId());
+ assertEquals(USER_ID, result.actorUserId());
+ assertEquals("model-v1", result.authorizationModelId());
+ assertEquals(EVALUATED_AT, result.evaluatedAt());
+ assertEquals(Set.of(ASSET_ID), result.assetIdsByKnowledgeSpace().get(SPACE_ID));
+ assertEquals(7L, result.authorizationGeneration(SPACE_ID));
+ assertThrows(
+ UnsupportedOperationException.class,
+ () -> result.assetIdsByKnowledgeSpace().put(UUID.randomUUID(), Set.of()));
+ assertThrows(
+ UnsupportedOperationException.class,
+ () -> result.assetIdsByKnowledgeSpace().get(SPACE_ID).add(UUID.randomUUID()));
+ }
+
+ @Test
+ void translatesScopeResolutionFailureToTheStableRetrievalException() {
+ var cause = new KnowledgeEvidenceScopeUnavailableException(
+ "AUTHORIZED_OBJECT_SET_INVALID", "model-v1");
+ when(evidenceScopes.resolve(actor, "model-v1")).thenThrow(cause);
+
+ KnowledgeRetrievalUnavailableException thrown = assertThrows(
+ KnowledgeRetrievalUnavailableException.class,
+ () -> verifier.verifyScope(actor, "model-v1"));
+
+ assertSame(cause, thrown.getCause());
+ }
+
+ @Test
+ void acceptsOnlyTheExactCurrentCanonicalEvidenceIdentity() {
+ VerifiedGraphEvidenceScope scope = verifier.verifyScope(actor, "model-v1");
+ EvidenceReference evidence = evidence();
+ when(canonicalEvidence.recheck(any(), any()))
+ .thenReturn(List.of(candidate(
+ ORGANIZATION_ID,
+ CHUNK_ID,
+ ASSET_ID,
+ REVISION_ID,
+ ACL_ID)));
+
+ assertTrue(verifier.isCurrentGoverningEvidence(scope, SPACE_ID, evidence));
+
+ ArgumentCaptor retrievalScope =
+ ArgumentCaptor.forClass(SecureKnowledgeRetrievalStore.RetrievalScope.class);
+ verify(canonicalEvidence).recheck(retrievalScope.capture(), eq(List.of(CHUNK_ID)));
+ assertEquals(ORGANIZATION_ID, retrievalScope.getValue().organizationId());
+ assertEquals(List.of(ASSET_ID), retrievalScope.getValue().authorizedAssetIds());
+ assertEquals("model-v1", retrievalScope.getValue().authorizationModelId());
+ }
+
+ @Test
+ void rejectsEvidenceOutsideTheVerifiedSpaceWithoutTouchingTheStore() {
+ VerifiedGraphEvidenceScope scope = verifier.verifyScope(actor, "model-v1");
+
+ assertFalse(verifier.isCurrentGoverningEvidence(
+ scope, UUID.randomUUID(), evidence()));
+ assertFalse(verifier.isCurrentGoverningEvidence(
+ scope,
+ SPACE_ID,
+ new EvidenceReference(
+ ORGANIZATION_ID,
+ UUID.randomUUID(),
+ REVISION_ID,
+ CHUNK_ID,
+ ACL_ID,
+ 7)));
+
+ verify(canonicalEvidence, never()).recheck(any(), any());
+ }
+
+ @Test
+ void rejectsMissingDuplicateOrMismatchedCanonicalCandidates() {
+ VerifiedGraphEvidenceScope scope = verifier.verifyScope(actor, "model-v1");
+ EvidenceReference evidence = evidence();
+ SecureRetrievalCandidate exact = candidate(
+ ORGANIZATION_ID,
+ CHUNK_ID,
+ ASSET_ID,
+ REVISION_ID,
+ ACL_ID);
+ when(canonicalEvidence.recheck(any(), any()))
+ .thenReturn(
+ List.of(),
+ List.of(exact, exact),
+ List.of(candidate(
+ UUID.randomUUID(),
+ CHUNK_ID,
+ ASSET_ID,
+ REVISION_ID,
+ ACL_ID)),
+ List.of(candidate(
+ ORGANIZATION_ID,
+ UUID.randomUUID(),
+ ASSET_ID,
+ REVISION_ID,
+ ACL_ID)),
+ List.of(candidate(
+ ORGANIZATION_ID,
+ CHUNK_ID,
+ ASSET_ID,
+ UUID.randomUUID(),
+ ACL_ID)),
+ List.of(candidate(
+ ORGANIZATION_ID,
+ CHUNK_ID,
+ ASSET_ID,
+ REVISION_ID,
+ UUID.randomUUID())),
+ List.of(candidate(
+ ORGANIZATION_ID,
+ CHUNK_ID,
+ UUID.randomUUID(),
+ REVISION_ID,
+ ACL_ID)));
+
+ assertFalse(verifier.isCurrentGoverningEvidence(scope, SPACE_ID, evidence));
+ assertFalse(verifier.isCurrentGoverningEvidence(scope, SPACE_ID, evidence));
+ assertFalse(verifier.isCurrentGoverningEvidence(scope, SPACE_ID, evidence));
+ assertFalse(verifier.isCurrentGoverningEvidence(scope, SPACE_ID, evidence));
+ assertFalse(verifier.isCurrentGoverningEvidence(scope, SPACE_ID, evidence));
+ assertFalse(verifier.isCurrentGoverningEvidence(scope, SPACE_ID, evidence));
+ assertFalse(verifier.isCurrentGoverningEvidence(scope, SPACE_ID, evidence));
+ }
+
+ @Test
+ void comparesGraphSnapshotsUsingTheCallersExistingSemantics() {
+ VerifiedGraphEvidenceScope initial = verifier.verifyScope(actor, "model-v1");
+ VerifiedGraphEvidenceScope same = snapshot("model-v1", Set.of(ASSET_ID), 7L);
+ VerifiedGraphEvidenceScope newGeneration = snapshot("model-v1", Set.of(ASSET_ID), 8L);
+ VerifiedGraphEvidenceScope newModel = snapshot("model-v2", Set.of(ASSET_ID), 7L);
+
+ assertTrue(initial.hasSameAuthorizationFingerprint(same, SPACE_ID));
+ assertTrue(initial.hasSameAssetsAndGeneration(same, SPACE_ID));
+ assertTrue(initial.hasSameSpaceScope(same, SPACE_ID));
+ assertFalse(initial.hasSameAuthorizationFingerprint(newGeneration, SPACE_ID));
+ assertFalse(initial.hasSameAssetsAndGeneration(newGeneration, SPACE_ID));
+ assertFalse(initial.hasSameAuthorizationFingerprint(newModel, SPACE_ID));
+ assertTrue(initial.hasSameAssetsAndGeneration(newModel, SPACE_ID));
+ assertFalse(initial.hasSameSpaceScope(newModel, SPACE_ID));
+ UUID unknownSpaceId = UUID.randomUUID();
+ assertFalse(initial.hasSameSpaceScope(same, unknownSpaceId));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> initial.forKnowledgeSpace(unknownSpaceId));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> initial.authorizationGeneration(unknownSpaceId));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> new VerifiedGraphEvidenceScope(
+ ORGANIZATION_ID,
+ USER_ID,
+ null,
+ false,
+ "model-v1",
+ EVALUATED_AT,
+ Map.of(SPACE_ID, Set.of(ASSET_ID)),
+ Map.of()));
+ }
+
+ @Test
+ void canonicalRecheckScopeContainsOnlyAssetsFromTheRequestedSpace() {
+ UUID otherSpaceId = UUID.randomUUID();
+ UUID otherAssetId = UUID.randomUUID();
+ var scope = new VerifiedGraphEvidenceScope(
+ ORGANIZATION_ID,
+ USER_ID,
+ null,
+ false,
+ "model-v1",
+ EVALUATED_AT,
+ Map.of(
+ SPACE_ID, Set.of(ASSET_ID),
+ otherSpaceId, Set.of(otherAssetId)),
+ Map.of(
+ SPACE_ID, 7L,
+ otherSpaceId, 3L));
+
+ assertEquals(
+ List.of(ASSET_ID),
+ scope.toRetrievalScope(SPACE_ID).authorizedAssetIds());
+ }
+
+ private static ResolvedKnowledgeEvidenceScope resolvedScope() {
+ return new ResolvedKnowledgeEvidenceScope(
+ ORGANIZATION_ID,
+ USER_ID,
+ null,
+ false,
+ "model-v1",
+ EVALUATED_AT,
+ Map.of(SPACE_ID, Set.of(ASSET_ID)),
+ Map.of(SPACE_ID, 7L));
+ }
+
+ private static VerifiedGraphEvidenceScope snapshot(
+ String authorizationModelId,
+ Set assets,
+ long generation) {
+ return new VerifiedGraphEvidenceScope(
+ ORGANIZATION_ID,
+ USER_ID,
+ null,
+ false,
+ authorizationModelId,
+ EVALUATED_AT,
+ Map.of(SPACE_ID, assets),
+ Map.of(SPACE_ID, generation));
+ }
+
+ private static EvidenceReference evidence() {
+ return new EvidenceReference(
+ ORGANIZATION_ID,
+ ASSET_ID,
+ REVISION_ID,
+ CHUNK_ID,
+ ACL_ID,
+ 7);
+ }
+
+ private static SecureRetrievalCandidate candidate(
+ UUID organizationId,
+ UUID chunkId,
+ UUID assetId,
+ UUID revisionId,
+ UUID currentAclSnapshotId) {
+ return new SecureRetrievalCandidate(
+ organizationId,
+ chunkId,
+ assetId,
+ SOURCE_OBJECT_ID,
+ revisionId,
+ "Policy",
+ "Approved policy",
+ "source://policy",
+ null,
+ null,
+ null,
+ 0,
+ currentAclSnapshotId,
+ currentAclSnapshotId,
+ "model-v1",
+ PROFILE_ID,
+ 1);
+ }
+}
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 feab5853..b2c98f42 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
@@ -1171,7 +1171,7 @@ unaffected jobs skipped by surface detection. CodeRabbit was rate limited;
direct audit found no defect, review, inline comment, or review thread. Both the
PR head `b5428d33` and merge commit are ancestors of current `origin/main`.
-## Current Pull Request Gates
+## Thirty-ninth Pull Request Evidence
- Source Ledger owns one typed citation-evidence query that resolves a
tenant-scoped ready revision, matching Knowledge Asset, and validated evidence
@@ -1207,3 +1207,80 @@ After merging current `origin/main` at `39281c33`, the Citation plus full
Modulith slice passed again in 55s. The documentation check passed across the
new base's 485 Markdown files and 8 mirrored domain pairs, and all 41
release-policy tests passed again on Node 24.15.0.
+
+PR #258 merged as `6ed738c2` after Backend Java 25, documentation, evaluation,
+secret, impact, release-preview, release-policy, and aggregate CI checks passed;
+unaffected jobs skipped by surface detection. CodeRabbit was rate limited;
+direct audit against the architecture verdict found no defect, review, inline
+comment, or review thread. Both the PR head `815640cd` and merge commit are
+ancestors of current `origin/main`.
+
+## Current Pull Request Gates
+
+- Retrieval owns one `GraphEvidenceVerifier` contract and immutable
+ `VerifiedGraphEvidenceScope`; its package-private implementation alone may use
+ `KnowledgeEvidenceScopeResolver`, `ResolvedKnowledgeEvidenceScope`,
+ `SecureKnowledgeRetrievalStore`, its retrieval scope, or
+ `SecureRetrievalCandidate`.
+- Graph exploration and export use verified per-Space evidence snapshots and
+ retain their existing before/after authorization comparison and retry/fail
+ behavior. Curation uses the verifier for governing chunk freshness and retains
+ its stricter authorized-asset plus ACL-generation comparison.
+- Graph imports no Retrieval resolver, resolved scope, store, store scope, or
+ candidate. Its remaining Retrieval dependencies are the verifier/snapshot,
+ existing retrieval-unavailable exception, and embedding profile contracts.
+- Current authorization remains resolved before Graph reads; governing evidence
+ must still match organization, Asset, revision, ACL snapshot, and chunk after a
+ canonical store recheck.
+- This code PR remains below 100 changed paths. Retrieval stays open for the
+ remaining API/Worker adapter interfaces and final closure.
+
+Local verification starts by changing the exact Graph-to-Retrieval dependency
+test to the intended verifier-only surface and observing it fail against the
+current resolver/store/candidate imports. The verifier, all three Graph use
+cases, and both exact Modulith guards then passed their focused slice. The first
+full Core run exposed the second temporary-open-boundary allowlist that still
+named the retired Graph dependencies; after aligning that guard, the two
+structural tests and full Core rerun passed. That first run also exhausted native
+JVM memory while two unrelated worktrees were running Gradle concurrently; the
+isolated sequential rerun passed in 2m10s. Full API and Worker reruns passed in
+5m and 2m36s, including deployable Spring wiring. The documentation
+operating-model check passed for 486 Markdown files and 8 mirrored domain pairs.
+Release policy passed all 41 tests on Node 24.15.0. The mechanical audit found
+20 changed paths, no migration, no empty changed file, no forbidden Graph import
+of Retrieval implementation types, and a clean whitespace diff. The terminating
+sequential `clean test` passed with 99 actionable tasks in 2m05s; after the
+verifier test was strengthened to cover organization and chunk mismatches, its
+focused rerun stayed green and a fresh terminating `clean test` passed all 99
+tasks again in 1m41s.
+
+After merging current `origin/main` at `f2cf3c67`, the four Graph/verifier test
+classes plus the full Modulith verification slice passed in 1m35s. The
+documentation operating-model check passed on the merged base for 501 Markdown
+files and 8 mirrored domain pairs, and all 41 release-policy tests passed again
+on Node 24.15.0.
+
+PR CI's first product-release job passed its contract tests but rejected the
+missing release disposition in the PR event payload. The PR now explicitly
+skips an intermediate release because the project owner requested one release
+only after the full refactor goal; a new synchronize event is required because
+rerunning the original workflow retains its original PR payload.
+
+CodeRabbit then found a valid fail-closed gap: an absent Space could degrade to
+an empty asset set and generation zero, allowing export comparison or
+deactivation guards to treat two absent scopes as stable. The fix makes snapshot
+accessors reject unknown Spaces, explicitly denies export/deactivation before
+read or write, and narrows canonical evidence rechecks to the requested Space's
+assets. Candidate identity tests now vary organization, chunk, Asset, revision,
+and current ACL independently; curation tests cover stale evidence and absent
+Space deactivation. The duplicated Graph-side unavailable-scope translation and
+Space comparison rules were consolidated to avoid authorization drift. The new
+tests failed first against the permissive/default APIs; the corrected Graph,
+verifier, and full Modulith slice passed in 35s, followed by full Core in 1m43s.
+The terminating post-review `clean test` then passed all 99 tasks in 6m31s,
+including uncached API and Worker tests affected by the Core boundary change.
+
+After a second main sync at `8bf800c6` brought the governed document-action
+Retrieval changes, the Graph/verifier classes plus full Modulith slice passed
+again in 51s. Documentation passed for 506 Markdown files and 8 mirrored domain
+pairs, and all 41 release-policy tests passed on Node 24.15.0.
diff --git a/docs/specs/domains/secure-graph-rag.md b/docs/specs/domains/secure-graph-rag.md
index b6916526..6b48a076 100644
--- a/docs/specs/domains/secure-graph-rag.md
+++ b/docs/specs/domains/secure-graph-rag.md
@@ -8,7 +8,7 @@ payload-boundary configuration this document states —
`apps/api/src/main/resources/application*.yml` and
`apps/worker/src/main/resources/application*.yml`.
-Reconciled: `2026-08-02 integrated publication lifecycle and graph extraction route (e6b5d51d)`.
+Reconciled: `2026-08-02-spring-modulith-package-refactor (f2cf3c67)`.
## Current Contract
@@ -21,6 +21,13 @@ Reconciled: `2026-08-02 integrated publication lifecycle and graph extraction ro
- Every graph read requires an `AuthorizedEvidenceScope`; ranking, adjacency,
degree, weight, aggregation, and citations can use only visible
contributions.
+- Graph exploration, export, and curation obtain the canonical authorization
+ snapshot through Retrieval's `GraphEvidenceVerifier`. Only its immutable
+ `VerifiedGraphEvidenceScope` crosses the module boundary. Retrieval alone
+ resolves the scope and rechecks an exact governing chunk against the current
+ canonical organization, Asset, revision, and ACL identity. Snapshot accessors
+ reject an unverified Knowledge Space, and the canonical recheck is narrowed
+ to the assets authorized for that exact Space.
- `SECURE_MIX` is the product default. Strategy selection remains internal.
- Query results preserve structured entity, relation, and chunk selections.
Entity and relation descriptions retain their individual chunk evidence;
diff --git a/docs/specs/domains/secure-retrieval.md b/docs/specs/domains/secure-retrieval.md
index ba9ef4b9..3b453906 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-02-effective-access-inspector-main-sync (946feb7c)`.
+Reconciled: `2026-08-02-spring-modulith-package-refactor (f2cf3c67)`.
## Current Behavior
@@ -40,6 +40,15 @@ state through Organization-owned queries before resolving evidence or source
visibility. It does not trust those actor fields as authorization facts and
imports no Organization entity, role, or repository.
+Graph exploration, export, and curation cross the Retrieval-owned
+`GraphEvidenceVerifier` and immutable `VerifiedGraphEvidenceScope`. The
+package-private implementation alone resolves canonical authorization state
+and rechecks governing evidence through the secure retrieval store. Graph
+imports neither the scope resolver, internal resolved scope, store, nor secure
+candidate representation. Unknown Knowledge Spaces are rejected rather than
+degrading to an empty/zero scope, and each governing-evidence recheck contains
+only the Asset IDs authorized for the requested Space.
+
Citation URLs are opaque API routes, not object-storage URLs. Opening one reruns
the current canonical evidence boundary once, validates the revision and blob
integrity, and streams the original bytes through the authenticated API with
diff --git a/docs/tests/domains/secure-graph-rag.md b/docs/tests/domains/secure-graph-rag.md
index 272d005d..bf8e0a9d 100644
--- a/docs/tests/domains/secure-graph-rag.md
+++ b/docs/tests/domains/secure-graph-rag.md
@@ -9,7 +9,7 @@ Source: `components/graph-rag-core/src/test`,
`core/src/test/java/com/orgmemory/core/knowledge`, and
`apps/web/test/e2e`.
-Reconciled: `2026-08-02 integrated publication lifecycle and graph extraction route (e6b5d51d)`.
+Reconciled: `2026-08-02-spring-modulith-package-refactor (f2cf3c67)`.
## Automated
@@ -20,6 +20,12 @@ Reconciled: `2026-08-02 integrated publication lifecycle and graph extraction ro
explicitly overridable. Production Compose validation checks the same route.
- Graph-testkit security tests prove permission-scoped contribution,
adjacency, degree, weight, seed, replacement, and removal behavior.
+- Core verifier and Graph use-case tests prove immutable authorized snapshots,
+ unavailable-scope translation, exact current governing-evidence identity,
+ per-Space canonical recheck scope, rejection of unknown Spaces before export
+ or deactivation, before/after authorization checks, and no Graph dependency
+ on Retrieval's resolver, resolved scope, store, or candidate implementation
+ types.
- Core traversal tests prove exact-snapshot validation before zero/empty
returns, authorized seed normalization, multi-page completion, canonical UUID
ordering, one global limit, cycles, disconnected nodes, seed permutations,
diff --git a/docs/tests/domains/secure-retrieval.md b/docs/tests/domains/secure-retrieval.md
index 3934dfec..06af480d 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-02-effective-access-inspector-main-sync (946feb7c)`.
+Reconciled: `2026-08-02-spring-modulith-package-refactor (f2cf3c67)`.
Primary evidence: `apps/api/src/test/java/com/orgmemory/api/knowledge/KnowledgeRetrievalIntegrationTests.java` and `core/src/test/java/com/orgmemory/core/permission/KnowledgePermissionPolicyTests.java`.
@@ -27,6 +27,7 @@ Primary evidence: `apps/api/src/test/java/com/orgmemory/api/knowledge/KnowledgeR
| 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` |
| Retrieval reloads active persisted department and Executive facts through Organization-owned queries and imports no Organization persistence or role types | `JpaKnowledgeAccessSubjectQueryTests`, `JpaOrganizationResourceQueryTests`, `ModulithVerificationTests#retrievalDoesNotDependOnOrganizationPersistenceOrRoleTypes`, `#retrievalOrganizationReadsUseOnlyOwnerQueries`, `SecureSourceVisibilityAdapterTests`, `KnowledgeRetrievalIntegrationTests` |
| Citation opening consumes Source Ledger-owned immutable evidence without revision/blob persistence leakage, preserves typed unavailable audit reasons, and closes integrity-mismatched content before an allow audit | `SourceCitationEvidenceQueryTests`, `CitationContentServiceTests`, `ModulithVerificationTests#retrievalDoesNotDependOnSourceLedgerCitationPersistenceOrStatusTypes`, `#citationContentUsesTheSourceLedgerOwnerQuery` |
+| Graph exploration, export, and curation consume only Retrieval's verifier and immutable snapshot; unknown Spaces fail closed, rechecks carry only the requested Space's assets, and the verifier accepts only one exact current governing-evidence candidate | `CanonicalGraphEvidenceVerifierTests`, `KnowledgeGraphExplorerServiceTests`, `KnowledgeGraphExportServiceTests`, `KnowledgeGraphCurationServiceTests`, `ModulithVerificationTests#graphConsumesOnlyRetrievalGraphContracts` |
Request-boundary missing control role/incomplete actor returns `403`; generic
resource `404` does not claim otherwise. Provider-backed evaluation,