RelationBranchProjectionRebuildService.java
package com.taxonomy.relations.service;
import com.taxonomy.dsl.ast.BlockAst;
import com.taxonomy.dsl.ast.DocumentAst;
import com.taxonomy.dsl.parser.TaxDslParser;
import com.taxonomy.dsl.storage.DslGitRepository;
import com.taxonomy.dsl.storage.DslGitRepositoryFactory;
import com.taxonomy.dsl.storage.ExpectedHeadDslCommitter;
import com.taxonomy.model.RelationType;
import com.taxonomy.workspace.service.RepositoryContext;
import com.taxonomy.workspace.service.RepositoryScope;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Ref;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
/**
* Rebuilds one complete branch relation projection from authoritative TaxDSL.
*
* <p>Repository resolution, branch-head capture, DSL parsing and semantic
* validation happen before the transactional writer replaces any database rows.
* The branch head is verified again immediately before the writer is invoked.
* A later read boundary still compares the persisted checkpoint with the live
* branch head, so a commit that arrives after the rebuild cannot expose stale
* rows as current.</p>
*/
@Service
public class RelationBranchProjectionRebuildService {
private static final String RELATION_KIND = "relation";
private final DslGitRepositoryFactory gitRepositoryFactory;
private final RelationBranchProjectionRebuildWriter rebuildWriter;
private final TaxDslParser parser;
private final ExpectedHeadDslCommitter expectedHeadVerifier;
@Autowired
public RelationBranchProjectionRebuildService(
DslGitRepositoryFactory gitRepositoryFactory,
RelationBranchProjectionRebuildWriter rebuildWriter) {
this(
gitRepositoryFactory,
rebuildWriter,
new TaxDslParser(),
new ExpectedHeadDslCommitter());
}
RelationBranchProjectionRebuildService(
DslGitRepositoryFactory gitRepositoryFactory,
RelationBranchProjectionRebuildWriter rebuildWriter,
TaxDslParser parser,
ExpectedHeadDslCommitter expectedHeadVerifier) {
this.gitRepositoryFactory = Objects.requireNonNull(
gitRepositoryFactory, "gitRepositoryFactory");
this.rebuildWriter = Objects.requireNonNull(
rebuildWriter, "rebuildWriter");
this.parser = Objects.requireNonNull(parser, "parser");
this.expectedHeadVerifier = Objects.requireNonNull(
expectedHeadVerifier, "expectedHeadVerifier");
}
/** Replaces the exact branch projection and writes its completion checkpoint atomically. */
public RebuildResult rebuild(RepositoryContext context) {
Objects.requireNonNull(context, "context");
requireMutable(context);
DslGitRepository repository = gitRepositoryFactory.resolveRepository(context);
String head = readHead(repository, context.branch());
String dsl = readDsl(repository, head);
List<RelationSnapshot> relations = parseRelations(dsl, head);
verifyHead(repository, context.branch(), head);
return rebuildWriter.replace(context, head, relations);
}
private String readHead(DslGitRepository repository, String branch) {
try {
Ref ref = repository.getGitRepository().getRefDatabase()
.exactRef(Constants.R_HEADS + branch);
if (ref == null || ref.getObjectId() == null) {
throw new BranchProjectionSourceException(
"Cannot rebuild relation projection: branch '"
+ branch + "' does not exist");
}
return ref.getObjectId().name();
} catch (IOException error) {
throw new BranchProjectionSourceException(
"Unable to read relation projection branch head '"
+ branch + "'",
error);
}
}
private String readDsl(DslGitRepository repository, String commitId) {
try {
String dsl = repository.getDslAtCommit(commitId);
if (dsl == null) {
throw new BranchProjectionSourceException(
"Authoritative commit has no architecture.taxdsl: "
+ commitId);
}
return dsl;
} catch (IOException error) {
throw new BranchProjectionSourceException(
"Unable to read authoritative relation projection commit "
+ commitId,
error);
}
}
private void verifyHead(
DslGitRepository repository,
String branch,
String expectedHead) {
try {
String verified = expectedHeadVerifier.verifyExpectedHead(
repository, branch, expectedHead);
if (!expectedHead.equals(verified)) {
throw new BranchProjectionSourceException(
"Selected branch no longer has the captured projection head");
}
} catch (IOException error) {
throw new BranchProjectionSourceException(
"Selected branch moved during relation projection rebuild",
error);
}
}
private List<RelationSnapshot> parseRelations(String dsl, String commitId) {
DocumentAst document;
try {
document = parser.parse(dsl, "architecture.taxdsl");
} catch (RuntimeException error) {
throw new BranchProjectionSourceException(
"Cannot rebuild relation projection from invalid TaxDSL commit "
+ commitId,
error);
}
Map<RelationIdentity, RelationSnapshot> relations = new LinkedHashMap<>();
for (BlockAst block : document.getBlocks()) {
if (!RELATION_KIND.equals(block.getKind())) {
continue;
}
List<String> tokens = block.getHeaderTokens();
if (tokens.size() != 3) {
throw new BranchProjectionSourceException(
"Relation block must contain exactly source, type and target at commit "
+ commitId);
}
RelationType relationType;
try {
relationType = RelationType.valueOf(
tokens.get(1).toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException error) {
throw new BranchProjectionSourceException(
"Unsupported relation type '" + tokens.get(1)
+ "' at commit " + commitId,
error);
}
RelationIdentity identity = new RelationIdentity(
requireToken(tokens.get(0), "source code"),
relationType,
requireToken(tokens.get(2), "target code"));
RelationSnapshot snapshot = new RelationSnapshot(
identity.sourceCode(),
identity.relationType(),
identity.targetCode(),
uniqueProperty(block, "status", identity),
confidence(block, identity),
uniqueProperty(block, "provenance", identity));
if (relations.putIfAbsent(identity, snapshot) != null) {
throw new BranchProjectionSourceException(
"Duplicate relation in authoritative branch projection: "
+ identity.display());
}
}
return List.copyOf(relations.values());
}
private static String uniqueProperty(
BlockAst block,
String property,
RelationIdentity identity) {
List<String> values = block.propertyValues(property);
if (values.size() > 1) {
throw new BranchProjectionSourceException(
"Duplicate " + property + " property on relation "
+ identity.display());
}
return values.isEmpty() ? null : normalizeOptional(values.getFirst());
}
private static Double confidence(
BlockAst block,
RelationIdentity identity) {
String value = uniqueProperty(block, "confidence", identity);
if (value == null) {
return null;
}
try {
double parsed = Double.parseDouble(value);
if (!Double.isFinite(parsed) || parsed < 0.0 || parsed > 1.0) {
throw new NumberFormatException("outside [0,1]");
}
return parsed;
} catch (NumberFormatException error) {
throw new BranchProjectionSourceException(
"Invalid confidence on relation " + identity.display(),
error);
}
}
private static void requireMutable(RepositoryContext context) {
if (context.scope() == RepositoryScope.CENTRAL_READ) {
throw new BranchProjectionContextException(
"Relation projection rebuild requires a writable repository context");
}
}
private static String requireToken(String value, String field) {
if (value == null || value.isBlank()) {
throw new BranchProjectionSourceException(field + " must not be blank");
}
String normalized = value.strip();
if (normalized.chars().anyMatch(Character::isWhitespace)) {
throw new BranchProjectionSourceException(
field + " must be one TaxDSL token: " + normalized);
}
return normalized;
}
private static String normalizeOptional(String value) {
return value == null || value.isBlank() ? null : value.strip();
}
public record RelationSnapshot(
String sourceCode,
RelationType relationType,
String targetCode,
String status,
Double confidence,
String provenance) {
public RelationSnapshot {
sourceCode = requireToken(sourceCode, "sourceCode");
relationType = Objects.requireNonNull(relationType, "relationType");
targetCode = requireToken(targetCode, "targetCode");
status = normalizeOptional(status);
provenance = normalizeOptional(provenance);
if (confidence != null
&& (!Double.isFinite(confidence)
|| confidence < 0.0
|| confidence > 1.0)) {
throw new IllegalArgumentException(
"confidence must be finite and between 0.0 and 1.0");
}
}
}
public record RebuildResult(
String repositoryId,
String workspaceId,
String branch,
String authoritativeCommitId,
int relationCount) {
}
private record RelationIdentity(
String sourceCode,
RelationType relationType,
String targetCode) {
private String display() {
return sourceCode + " " + relationType + " " + targetCode;
}
}
public static final class BranchProjectionContextException
extends IllegalArgumentException {
public BranchProjectionContextException(String message) {
super(message);
}
}
public static final class BranchProjectionSourceException
extends IllegalStateException {
public BranchProjectionSourceException(String message) {
super(message);
}
public BranchProjectionSourceException(String message, Throwable cause) {
super(message, cause);
}
}
}