IntegrationContracts.java

package com.taxonomy.extension.api.integration;

import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;

/** Framework-neutral exchange values. These are transport/evidence values, never a second authoring model. */
public final class IntegrationContracts {
    private IntegrationContracts() {}

    public enum AuthorityMode { LINK_ONLY, IMPORT_COPY, MIRROR_READ, PUBLISH_TARGET, BIDIRECTIONAL }
    public enum Capability { FILE_IMPORT, FILE_EXPORT, DISCOVERY, READ_LINK, CONDITIONAL_PUBLISH }
    public enum ArtifactKind { REQUIREMENT, ELEMENT, VIEW, SPECIFICATION, RELATION, PLACEMENT, METADATA }
    public enum ChangeKind { ADD, UPDATE, MOVE, REMOVE_CANDIDATE, RELATION, CONFLICT, UNCHANGED }
    public enum Decision { ACCEPT, REJECT, TAKE_EXTERNAL, KEEP_INTERNAL }
    public enum OperationStatus { FETCH_PENDING, FETCH_FAILED, PREVIEWED, APPLYING, APPLIED, CHECKPOINT_PENDING, COMPLETED, CONFLICT, CANCELLED, FAILED, PARTIAL }
    public enum LossDisposition { MAPPED, PRESERVED_EXTENSION, TRANSFORMED, UNSUPPORTED }

    public record IntegrationDescriptor(String id, String version, String title,
                                        Set<Capability> capabilities, Set<String> mediaTypes) {
        public IntegrationDescriptor {
            require(id, "connector id"); require(version, "connector version");
            capabilities = Set.copyOf(capabilities); mediaTypes = Set.copyOf(mediaTypes);
        }
    }

    /** Both editing revision and checkpoint matter; a Git SHA is never a semantic operation identity. */
    public record InternalState(String repositoryId, String workspaceScopeKey, String branch,
                                String commitId, long semanticRevision, Long projectId,
                                String projectFingerprint) {
        public InternalState {
            require(repositoryId, "repositoryId"); require(workspaceScopeKey, "workspaceScopeKey");
            require(branch, "branch");
            if (semanticRevision < 0) throw new IllegalArgumentException("Negative semantic revision");
        }
    }

    public record ExternalScope(String systemType, String repository, String configuration) {
        public ExternalScope { require(systemType, "external system"); require(repository, "external repository"); }
    }

    /** Credentials and private endpoint settings deliberately have no fields in this value. */
    public record IntegrationContext(UUID connectionId, AuthorityMode authority, ExternalScope externalScope,
                                     InternalState internalState, String actor, String profile, String profileVersion) {
        public IntegrationContext {
            Objects.requireNonNull(connectionId); Objects.requireNonNull(authority);
            Objects.requireNonNull(externalScope); Objects.requireNonNull(internalState);
            require(actor, "actor"); require(profile, "profile"); require(profileVersion, "profileVersion");
        }
    }

    public record ExternalArtifactReference(UUID connectionId, ExternalScope scope, String resourceId,
                                            String externalVersion, String contentFingerprint,
                                            InternalState internalState, String businessIdentity,
                                            String profile, String profileVersion, UUID lastOperationId) {}

    public record Artifact(String id, ArtifactKind kind, String type, String title, String text,
                           Map<String, String> attributes, Map<String, String> extensions) {
        public Artifact {
            require(id, "external id"); Objects.requireNonNull(kind);
            attributes = immutable(attributes); extensions = immutable(extensions);
            title = title == null ? "" : title; text = text == null ? "" : text;
        }
    }

    public record Relation(String id, String type, String source, String target,
                           Map<String, String> attributes, Map<String, String> extensions) {
        public Relation {
            require(id, "relation id"); require(source, "source"); require(target, "target");
            attributes = immutable(attributes); extensions = immutable(extensions);
        }
    }

    /** Occurrence identity preserves repeated requirements and independently named specification hierarchies. */
    public record Placement(String id, String containerId, String parentId, String artifactId, int position,
                            Map<String, String> attributes) {
        public Placement {
            require(id, "occurrence id"); require(containerId, "container id");
            if (position < 0) throw new IllegalArgumentException("Occurrence position must not be negative");
            attributes = immutable(attributes);
        }
    }

    public record MappingLoss(String artifactId, String field, String code,
                              LossDisposition disposition, String detail) {}

    /** Safe source is retained as exchange evidence; outbound adapters must overlay the frozen canonical values. */
    public record ExchangeDocument(String profile, String profileVersion, String externalVersion,
                                   boolean completeScope, String source,
                                   List<Artifact> artifacts, List<Relation> relations, List<Placement> placements,
                                   Map<String, String> metadata, List<MappingLoss> losses) {
        public ExchangeDocument {
            artifacts = List.copyOf(artifacts); relations = List.copyOf(relations);
            placements = List.copyOf(placements); metadata = immutable(metadata); losses = List.copyOf(losses);
        }
    }

    public record IntegrationChange(String id, String externalId, ChangeKind kind, Set<String> fields,
                                    String expectedInternalFingerprint, String expectedExternalFingerprint,
                                    Artifact before, Artifact after, List<String> conflicts) {
        public IntegrationChange { fields = Set.copyOf(fields); conflicts = List.copyOf(conflicts); }
    }

    public record IntegrationChangeSet(UUID operationId, IntegrationContext context, String fingerprint,
                                       List<IntegrationChange> changes, List<MappingLoss> losses,
                                       OperationStatus status, String resultCommit, Instant createdAt) {
        public IntegrationChangeSet { changes = List.copyOf(changes); losses = List.copyOf(losses); }
    }

    public record MappingOverride(String canonicalType, String titleAttribute, String textAttribute, String internalIdentity) {}
    public record ReviewedChangeSet(UUID operationId, String previewFingerprint,
                                    Map<String, Decision> decisions, String rationale, Map<String, MappingOverride> mappings) {
        public ReviewedChangeSet(UUID id, String fingerprint, Map<String, Decision> decisions, String rationale) {
            this(id, fingerprint, decisions, rationale, Map.of());
        }
        public ReviewedChangeSet {
            Objects.requireNonNull(operationId); require(previewFingerprint, "preview fingerprint");
            decisions = Map.copyOf(decisions); require(rationale, "rationale");
            mappings = mappings == null ? Map.of() : Map.copyOf(mappings);
            if (rationale.length() > 1000 || rationale.chars().anyMatch(Character::isISOControl))
                throw new IllegalArgumentException("Rationale must be bounded plain text");
        }
    }

    public record InboundRequest(IntegrationContext context, String mediaType, byte[] content,
                                 String externalVersion, boolean completeScope) {
        public InboundRequest { content = content.clone(); }
        @Override public byte[] content() { return content.clone(); }
    }
    public record OutboundRequest(IntegrationContext context, ExchangeDocument document, String expectedExternalVersion) {}
    public record ExchangeFile(String mediaType, String filename, byte[] content, List<MappingLoss> losses) {
        public ExchangeFile {
            require(filename, "filename");
            if (filename.length() > 240 || filename.equals(".") || filename.equals("..") || filename.contains("/") || filename.contains("\\")
                    || filename.chars().anyMatch(Character::isISOControl)) throw new IllegalArgumentException("Unsafe exchange filename");
            content = content.clone(); losses = List.copyOf(losses);
        }
        @Override public byte[] content() { return content.clone(); }
    }
    public record DiscoveryResource(String uri, String type, String title, String version, String configuration) {}
    public record DiscoveryResult(List<DiscoveryResource> resources, boolean complete, String version) {
        public DiscoveryResult { resources = List.copyOf(resources); }
    }
    /** Per-item outcomes make partial publication explicit; a timeout is never a successful checkpoint. */
    public record PublishItem(String resourceId, String expectedVersion, String resultVersion,
                              String contentFingerprint, OperationStatus status, String failureCode) {}
    public record PublishResult(UUID operationId, OperationStatus status, List<PublishItem> items) {
        public PublishResult { items = List.copyOf(items); }
    }

    private static Map<String, String> immutable(Map<String, String> value) {
        if (value == null) return Map.of();
        for (var entry : value.entrySet()) {
            if (entry.getKey() == null || entry.getValue() == null) {
                throw new IllegalArgumentException("Integration maps must not contain null keys or values");
            }
        }
        return Map.copyOf(value);
    }
    private static void require(String value, String field) {
        if (value == null || value.isBlank() || value.length() > 2048)
            throw new IllegalArgumentException("Missing or oversized " + field);
    }
}