GitAuthoritativeHypothesisService.java
package com.taxonomy.versioning.service;
import com.taxonomy.catalog.repository.TaxonomyNodeRepository;
import com.taxonomy.catalog.service.TaxonomyRelationService;
import com.taxonomy.dsl.storage.DslGitRepositoryFactory;
import com.taxonomy.dto.RelationHypothesisDto;
import com.taxonomy.model.RelationType;
import com.taxonomy.relations.command.ArchitectureRelationGitCommandService.CommandMetadata;
import com.taxonomy.relations.model.RelationHypothesis;
import com.taxonomy.relations.repository.RelationEvidenceRepository;
import com.taxonomy.relations.repository.RelationHypothesisRepository;
import com.taxonomy.relations.service.RelationBranchProjectionReadinessService;
import com.taxonomy.relations.service.RelationBranchProjectionReadinessService.Readiness;
import com.taxonomy.versioning.service.GitAuthoritativeHypothesisReviewService.ReviewAction;
import com.taxonomy.versioning.service.GitAuthoritativeHypothesisReviewService.ReviewResult;
import com.taxonomy.workspace.model.SystemRepository;
import com.taxonomy.workspace.model.UserWorkspace;
import com.taxonomy.workspace.repository.UserWorkspaceRepository;
import com.taxonomy.workspace.service.RepositoryContext;
import com.taxonomy.workspace.service.SystemRepositoryService;
import com.taxonomy.workspace.service.WorkspaceContext;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
/**
* Productive {@link HypothesisService} adapter that keeps historic call sites
* compatible while replacing their accept/reject implementation with the
* Git-authoritative command boundary.
*/
@Service
@Primary
public class GitAuthoritativeHypothesisService extends HypothesisService {
private final RelationHypothesisRepository hypothesisRepository;
private final GitAuthoritativeHypothesisReviewService reviewService;
private final RelationBranchProjectionReadinessService readinessService;
private final SystemRepositoryService systemRepositoryService;
private final UserWorkspaceRepository userWorkspaceRepository;
public GitAuthoritativeHypothesisService(
RelationHypothesisRepository hypothesisRepository,
RelationEvidenceRepository evidenceRepository,
TaxonomyRelationService relationService,
TaxonomyNodeRepository nodeRepository,
DslGitRepositoryFactory repositoryFactory,
SystemRepositoryService systemRepositoryService,
UserWorkspaceRepository userWorkspaceRepository,
GitAuthoritativeHypothesisReviewService reviewService,
RelationBranchProjectionReadinessService readinessService) {
super(
hypothesisRepository,
evidenceRepository,
relationService,
nodeRepository,
repositoryFactory,
systemRepositoryService,
userWorkspaceRepository);
this.hypothesisRepository = Objects.requireNonNull(
hypothesisRepository, "hypothesisRepository");
this.systemRepositoryService = Objects.requireNonNull(
systemRepositoryService, "systemRepositoryService");
this.userWorkspaceRepository = Objects.requireNonNull(
userWorkspaceRepository, "userWorkspaceRepository");
this.reviewService = Objects.requireNonNull(reviewService, "reviewService");
this.readinessService = Objects.requireNonNull(
readinessService, "readinessService");
}
/**
* Preserve the historic return value (new rows only), while enriching every
* supplied DTO with the exact persisted review identity. For a stable
* analysis session this also resolves rows created by an earlier retry.
*/
@Override
@Transactional
public List<RelationHypothesis> persistFromAnalysis(
List<RelationHypothesisDto> hypotheses,
String sessionId,
RepositoryContext context) {
RepositoryContext tenant = requireContext(context);
List<RelationHypothesis> persisted = super.persistFromAnalysis(
hypotheses, sessionId, tenant);
bindPersistedIds(hypotheses, sessionId, tenant, persisted);
return persisted;
}
@Override
@Transactional
public List<RelationHypothesis> persistFromAnalysis(
List<RelationHypothesisDto> hypotheses,
String sessionId,
WorkspaceContext workspaceContext) {
return persistFromAnalysis(
hypotheses,
sessionId,
resolveLegacyContext(workspaceContext));
}
@Override
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public RelationHypothesis accept(
Long hypothesisId,
RepositoryContext context) {
return reviewUnchecked(
hypothesisId,
requireContext(context),
ReviewAction.ACCEPT).hypothesis();
}
@Override
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public RelationHypothesis accept(
Long hypothesisId,
WorkspaceContext workspaceContext) {
return accept(hypothesisId, resolveLegacyContext(workspaceContext));
}
@Override
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public RelationHypothesis reject(
Long hypothesisId,
RepositoryContext context) {
return reviewUnchecked(
hypothesisId,
requireContext(context),
ReviewAction.REJECT).hypothesis();
}
@Override
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public RelationHypothesis reject(
Long hypothesisId,
WorkspaceContext workspaceContext) {
return reject(hypothesisId, resolveLegacyContext(workspaceContext));
}
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public RelationHypothesis revert(
Long hypothesisId,
RepositoryContext context) {
return reviewUnchecked(
hypothesisId,
requireContext(context),
ReviewAction.REVERT).hypothesis();
}
public ReviewResult review(
Long hypothesisId,
RepositoryContext context,
String expectedHeadCommit,
CommandMetadata metadata,
ReviewAction action) throws IOException {
return switch (Objects.requireNonNull(action, "action")) {
case ACCEPT -> reviewService.accept(
hypothesisId, context, expectedHeadCommit, metadata);
case REJECT -> reviewService.reject(
hypothesisId, context, expectedHeadCommit, metadata);
case REVERT -> reviewService.revert(
hypothesisId, context, expectedHeadCommit, metadata);
};
}
/** Validate exact tenant visibility and lifecycle before exposing branch state. */
public void requireReviewable(
Long hypothesisId,
RepositoryContext context,
ReviewAction action) {
reviewService.requireReviewable(
hypothesisId,
requireContext(context),
action);
}
private void bindPersistedIds(
List<RelationHypothesisDto> hypotheses,
String sessionId,
RepositoryContext context,
List<RelationHypothesis> newlyPersisted) {
if (hypotheses == null || hypotheses.isEmpty()) {
return;
}
String normalizedSession = normalizeOptional(sessionId);
List<RelationHypothesis> candidates = normalizedSession == null
? newlyPersisted
: hypothesisRepository
.findByAnalysisSessionIdInRepositoryWorkspace(
context.repositoryId(),
context.workspaceId(),
normalizedSession);
for (RelationHypothesisDto dto : hypotheses) {
if (dto == null) {
continue;
}
RelationType type;
try {
type = RelationType.valueOf(
requireText(dto.getRelationType(), "relationType")
.toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException error) {
continue;
}
candidates.stream()
.filter(entity -> entity.getRelationType() == type)
.filter(entity -> Objects.equals(
entity.getSourceNodeId(), dto.getSourceCode()))
.filter(entity -> Objects.equals(
entity.getTargetNodeId(), dto.getTargetCode()))
.findFirst()
.ifPresent(entity -> dto.setHypothesisId(entity.getId()));
}
}
private ReviewResult reviewUnchecked(
Long hypothesisId,
RepositoryContext context,
ReviewAction action) {
requireReviewable(hypothesisId, context, action);
Readiness readiness = readinessService.inspect(context);
String expectedHead = readiness.currentHeadCommit();
if (expectedHead == null) {
throw new IllegalStateException(
"Selected hypothesis branch does not have an authoritative Git head");
}
CommandMetadata metadata = new CommandMetadata(
"legacy-hypothesis-"
+ action.name().toLowerCase()
+ "-" + hypothesisId + "-" + expectedHead,
"Git-first hypothesis review through a compatibility call site");
try {
return review(
hypothesisId,
context,
expectedHead,
metadata,
action);
} catch (IOException error) {
throw new IllegalStateException(
"Git hypothesis review failed before an authority commit was created",
error);
}
}
private RepositoryContext resolveLegacyContext(WorkspaceContext workspaceContext) {
WorkspaceContext legacy = workspaceContext != null
? workspaceContext : WorkspaceContext.SHARED;
String username = normalizeUsername(legacy.username());
String branch = normalizeOptional(legacy.currentBranch());
String workspaceId = normalizeOptional(legacy.workspaceId());
if (workspaceId == null) {
SystemRepository primary = systemRepositoryService.getPrimaryRepository();
return RepositoryContext.centralRead(
primary.getRepositoryId(),
branch != null ? branch : primary.getDefaultBranch(),
username);
}
UserWorkspace workspace = userWorkspaceRepository.findByWorkspaceId(workspaceId)
.orElseThrow(() -> new IllegalArgumentException(
"Workspace not found while resolving repository context: "
+ workspaceId));
if (workspace.getUsername() != null
&& !workspace.getUsername().equals(username)
&& !"system".equals(username)) {
throw new IllegalArgumentException(
"Workspace does not belong to the active user: " + workspaceId);
}
String repositoryId = requireText(
workspace.getSourceRepositoryId(),
"workspace.sourceRepositoryId");
String workspaceBranch = branch != null
? branch : normalizeOptional(workspace.getCurrentBranch());
return RepositoryContext.workspace(
repositoryId,
workspaceId,
workspaceBranch != null ? workspaceBranch : "draft",
username);
}
private static RepositoryContext requireContext(RepositoryContext context) {
return Objects.requireNonNull(context, "context");
}
private static String normalizeUsername(String value) {
String normalized = normalizeOptional(value);
return normalized != null ? normalized : "system";
}
private static String normalizeOptional(String value) {
return value == null || value.isBlank() ? null : value.strip();
}
private static String requireText(String value, String field) {
String normalized = normalizeOptional(value);
if (normalized == null) {
throw new IllegalArgumentException(field + " must not be blank");
}
return normalized;
}
}