ArchitectureEditorService.java

package com.taxonomy.editor;

import com.taxonomy.dsl.command.ArchitectureCommand.*;
import com.taxonomy.dsl.command.ArchitectureDslCommands;
import com.taxonomy.dsl.command.ArchitectureDslCommands.Change;
import com.taxonomy.dsl.command.ArchitectureDslCommands.CommandProblem;
import com.taxonomy.dsl.command.ArchitectureSemanticPatch;
import com.taxonomy.dsl.storage.DslCommit;
import com.taxonomy.dsl.storage.DslGitRepository;
import com.taxonomy.dsl.storage.DslGitRepositoryFactory;
import com.taxonomy.editor.ArchitectureCommandPort.*;
import com.taxonomy.editor.persistence.EditorJournal;
import com.taxonomy.editor.persistence.EditorJournal.*;
import com.taxonomy.workspace.service.RepositoryContext;
import com.taxonomy.workspace.service.RepositoryScope;
import com.taxonomy.workspace.service.WorkspaceArchitectureVersionPort;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevWalk;
import org.springframework.stereotype.Service;

import java.io.OutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.DigestOutputStream;
import java.security.NoSuchAlgorithmException;
import java.util.*;

/** Accepted editing revisions belong to the durable workspace journal. Git contains explicit versions only. */
@Service
public class ArchitectureEditorService implements ArchitectureCommandPort, WorkspaceArchitectureVersionPort {
    private final DslGitRepositoryFactory repositories;
    private final EditorJournal journal;
    private final ArchitectureCheckpointWriter checkpointWriter;
    private final ArchitectureDslCommands transformer = new ArchitectureDslCommands();

    public ArchitectureEditorService(DslGitRepositoryFactory repositories, EditorJournal journal,
                                     ArchitectureCheckpointWriter checkpointWriter) {
        this.repositories = repositories; this.journal = journal; this.checkpointWriter = checkpointWriter;
    }

    public record HistoryEntry(String operationId, String commandId, String kind, String targetOperationId,
                               String actor, String occurredAt, String rationale, List<String> changedIds,
                               long previousRevision, long revision) {}
    public record Document(Context context, String dsl, String projectionState, List<HistoryEntry> history,
                           List<DslCommit> versions, long checkpointRevision, String pendingCheckpointId, String source) {}

    public Document read(RepositoryContext context, String commit) throws IOException { return read(context, commit, null); }

    public Document read(RepositoryContext context, String commit, Long revision) throws IOException {
        if (commit != null && revision != null) throw new IllegalArgumentException("Select a version or a semantic revision");
        DslGitRepository repository = repositories.resolveRepository(context);
        String head = head(repository, context.branch());
        List<DslCommit> versions = head == null ? List.of() : repository.getDslHistory(context.branch());
        if (commit != null) {
            String selected = ObjectId.fromString(commit).name();
            requireReachable(repository, head, selected);
            return new Document(Context.version(context, selected), source(repository, selected), "HISTORICAL",
                    List.of(), versions, 0, null, "GIT_CHECKPOINT");
        }
        Snapshot snapshot = snapshot(context);
        State state = snapshot.state();
        String dsl = state.dsl();
        long selected = revision == null ? state.revision() : revision;
        if (selected < 0 || selected > state.revision()) throw problem("NOT_FOUND", "revision", "Unknown workspace revision", List.of());
        if (selected != state.revision()) {
            dsl = journal.sourceAtRevision(context, selected);
            if (dsl == null) throw problem("NOT_FOUND", "revision", "Unknown workspace revision", List.of());
        }
        String status = selected != state.revision() ? "HISTORICAL"
                : state.pendingCheckpoint() != null ? "CHECKPOINT_PENDING"
                : !Objects.equals(head, state.checkpointCommit()) ? "VERSION_CHANGED" : "READY";
        return new Document(Context.of(context, state.checkpointCommit(), selected), dsl, status,
                snapshot.operations().stream().filter(e -> e.revision() <= selected && e.actor().equals(context.username()))
                        .limit(50).map(ArchitectureEditorService::history).toList(), versions,
                state.checkpointRevision(), state.pendingCheckpoint(), "WORKSPACE_REVISION");
    }

    @Override
    public Preview preview(RepositoryContext context, Command command) throws IOException {
        requireContext(context, command.context()); requireWritable(context);
        Snapshot snapshot = snapshot(context);
        expect(snapshot.state(), command.context().revision());
        requireInitialVersion(snapshot.state(), command.context());
        verifyVersion(context, snapshot.state());
        return new Preview(Context.of(context, snapshot.state().checkpointCommit(), snapshot.state().revision()),
                transform(context, command, snapshot.state().dsl(), snapshot.operations(), id -> journal.operation(context, id)),
                kind(command.operation()), target(command.operation()));
    }

    @Override
    public Accepted execute(RepositoryContext context, Command command) throws IOException {
        requireContext(context, command.context()); requireWritable(context);
        Snapshot initial = snapshot(context);
        String fingerprint = fingerprint(command);
        try {
            return journal.locked(context, initial.state(), session -> {
            Entry prior = session.find(command.metadata().commandId());
            if (prior != null) {
                requireFingerprint(fingerprint, prior.fingerprint());
                return accepted(context, session.state(), prior, true);
            }
            session.expect(command.context().revision());
            requireInitialVersion(session.state(), command.context());
            try { verifyVersion(context, session.state()); }
            catch (IOException failure) { throw new java.io.UncheckedIOException(failure); }
            Change change = transform(context, command, session.state().dsl(), session.operations(), session::find);
            if (change.changes().isEmpty()) throw problem("NO_CHANGE", "command", "No semantic change to accept", List.of());
            Entry entry = session.append(command.metadata(), context.username(), kind(command.operation()), target(command.operation()),
                    fingerprint, change.dsl(), List.copyOf(affected(change.changes())));
            return accepted(context, session.state(), entry, false);
            });
        } catch (java.io.UncheckedIOException failure) { throw failure.getCause(); }
    }

    /**
     * A reviewed integration is one explicit application boundary. Its typed architecture commands,
     * optional server-owned portfolio contribution and journal entry join the integration transaction.
     * The caller checkpoints separately after that transaction commits. No Git write happens here.
     */
    public Context acceptIntegration(RepositoryContext context, Context expected, Metadata metadata,
                                     String fingerprint, List<com.taxonomy.dsl.command.ArchitectureCommand> commands,
                                     java.util.function.UnaryOperator<String> portfolioContribution,
                                     Metadata checkpointMetadata) throws IOException {
        requireContext(context, expected); requireWritable(context);
        State initial = seed(context);
        return journal.joinedLocked(context, initial, session -> {
            Entry prior = session.find(metadata.commandId());
            if (prior != null) {
                requireFingerprint(prior.fingerprint(), fingerprint);
                return Context.of(context, session.state().checkpointCommit(), prior.revision());
            }
            session.expect(expected.revision()); requireInitialVersion(session.state(), expected);
            try { verifyVersion(context, session.state()); }
            catch (IOException failure) { throw new java.io.UncheckedIOException(failure); }
            String before = session.state().dsl(), next = before;
            for (var command : commands) next = transformer.apply(next, command).dsl();
            if (portfolioContribution != null) next = portfolioContribution.apply(next);
            if (!next.equals(before)) session.append(metadata, context.username(), "VERSION_IMPORT", null, fingerprint, next,
                    new ArrayList<>(affected(ArchitectureSemanticPatch.between(before, next))));
            Context result = Context.of(context, session.state().checkpointCommit(), session.state().revision());
            if (checkpointMetadata != null) session.prepare(checkpointMetadata, context.username(),
                    checkpointFingerprint(context, new CreateCheckpointCommand(result, checkpointMetadata)));
            return result;
        });
    }

    /** Workspace is the first model lock for cross-aggregate integrations, before any project lock. */
    public <T> T integrationBoundary(RepositoryContext context, java.util.function.Function<Document, T> action) throws IOException {
        requireWritable(context);
        return journal.joinedLocked(context, seed(context), session -> {
            State state = session.state();
            return action.apply(new Document(Context.of(context, state.checkpointCommit(), state.revision()), state.dsl(),
                    state.pendingCheckpoint() == null ? "READY" : "CHECKPOINT_PENDING", List.of(), List.of(), state.checkpointRevision(), state.pendingCheckpoint(), "WORKSPACE_REVISION"));
        });
    }

    private static Accepted accepted(RepositoryContext context, State state, Entry entry, boolean replayed) {
        return new Accepted(Context.of(context, state.checkpointCommit(), entry.revision()), entry.commandId(), entry.commandId(), replayed,
                "READY", new Change(entry.afterDsl(), ArchitectureSemanticPatch.between(entry.beforeDsl(), entry.afterDsl())));
    }

    /** Explicit checkpoint command: durable prepare, retryable Git write, durable completion. No semantic append. */
    public CheckpointAccepted checkpoint(RepositoryContext context, CreateCheckpointCommand command) throws IOException {
        requireContext(context, command.context()); requireWritable(context);
        // A pending intent must be recoverable even when its Git phase has already advanced HEAD.
        Snapshot current = journal.read(context);
        State initial = current == null ? seed(context) : current.state();
        String fingerprint = checkpointFingerprint(context, command);
        Checkpoint prepared;
        try {
            prepared = journal.locked(context, initial, session -> {
            Checkpoint prior = session.checkpoint(command.metadata().commandId());
            if (prior != null) {
                requireFingerprint(fingerprint, prior.fingerprint());
                if (prior.failureCode() != null) throw problem(prior.failureCode(), "checkpoint", "The checkpoint was rejected because Git moved; reconcile and create a new checkpoint", List.of(prior.commandId()));
                return prior;
            }
            session.expect(command.context().revision());
            try { verifyVersion(context, session.state()); }
            catch (IOException failure) { throw new java.io.UncheckedIOException(failure); }
            requireInitialVersion(session.state(), command.context());
            return session.prepare(command.metadata(), context.username(), fingerprint);
            });
        } catch (java.io.UncheckedIOException failure) { throw failure.getCause(); }
        boolean replayed = prepared.completed();
        Checkpoint completed = prepared;
        if (!prepared.completed()) {
            completed = finishCheckpoint(context, initial, prepared);
        }
        return new CheckpointAccepted(Context.of(context, completed.commitId(), completed.revision()), completed.commandId(),
                completed.commitId(), completed.fromRevision(), completed.revision(), completed.commitCreated(), replayed);
    }

    /** Resume the exact durable intent after restart; the authenticated actor must own it. */
    public CheckpointAccepted resumeCheckpoint(RepositoryContext context) throws IOException {
        requireWritable(context);
        Snapshot snapshot = journal.read(context);
        if (snapshot == null || snapshot.state().pendingCheckpoint() == null) throw problem("NOT_FOUND", "checkpoint", "No pending checkpoint", List.of());
        Checkpoint intent = journal.locked(context, snapshot.state(), session -> session.checkpoint(snapshot.state().pendingCheckpoint()));
        if (!context.username().equals(intent.actor())) throw problem("NOT_FOUND", "checkpoint", "No personal checkpoint", List.of());
        Checkpoint completed = finishCheckpoint(context, snapshot.state(), intent);
        return new CheckpointAccepted(Context.of(context, completed.commitId(), completed.revision()), completed.commandId(),
                completed.commitId(), completed.fromRevision(), completed.revision(), completed.commitCreated(), true);
    }

    private Checkpoint finishCheckpoint(RepositoryContext context, State initial, Checkpoint intent) throws IOException {
        try {
            var written = checkpointWriter.write(repositories.resolveRepository(context), EditorJournal.scope(context), context.branch(), intent);
            return journal.locked(context, initial, session -> session.complete(intent.commandId(), written.commitId(), written.created()));
        } catch (com.taxonomy.dsl.storage.ExpectedHeadDslCommitter.BranchHeadConflictException conflict) {
            // Deterministic commit recovery ran first: this exception proves the intent did not advance the ref.
            journal.locked(context, initial, session -> { session.rejectCheckpoint(intent.commandId()); return null; });
            throw problem("CHECKPOINT_CONFLICT", "checkpoint", "Git changed before this checkpoint could be applied; reconcile the versions", List.of(intent.commandId()));
        }
    }

    /** Editor projections are rebuilt from exact canonical DSL on every read, including before the first checkpoint. */
    public String rebuild(RepositoryContext context, Context expected) throws IOException {
        requireContext(context, expected); requireWritable(context);
        Snapshot snapshot = snapshot(context);
        expect(snapshot.state(), expected.revision());
        transformer.model(snapshot.state().dsl());
        return "READY";
    }

    @Override
    public <T> T version(RepositoryContext context, String rationale, GitAction<T> action) throws IOException {
        Snapshot snapshot = journal.read(context);
        if (context.scope() != RepositoryScope.WORKSPACE || snapshot == null) return action.run();
        if (snapshot.state().pendingCheckpoint() != null) resumeCheckpoint(context);
        reconcileVersion(context);
        snapshot = journal.read(context);
        String id = UUID.nameUUIDFromBytes((EditorJournal.scope(context) + ":" + snapshot.state().revision()
                + ":" + context.username() + ":" + rationale).getBytes(StandardCharsets.UTF_8)).toString();
        var prepared = checkpoint(context, new CreateCheckpointCommand(
                Context.of(context, snapshot.state().checkpointCommit(), snapshot.state().revision()), new Metadata(id, id, id, rationale)));
        try {
            return journal.locked(context, snapshot.state(), session -> {
                session.expect(prepared.context().revision());
                try {
                    T result = action.run();
                    importVersion(context, session, rationale);
                    return result;
                } catch (IOException failure) { throw new java.io.UncheckedIOException(failure); }
            });
        } catch (java.io.UncheckedIOException failure) { throw failure.getCause(); }
    }

    /** Recovery for a version write that reached Git before the journal transaction could finish. */
    public void reconcileVersion(RepositoryContext context) throws IOException {
        requireWritable(context);
        Snapshot snapshot = journal.read(context);
        if (snapshot == null) return;
        try {
            journal.locked(context, snapshot.state(), session -> {
                session.expect(session.state().revision());
                try {
                    String head = head(repositories.resolveRepository(context), context.branch());
                    if (Objects.equals(head, session.state().checkpointCommit())) return null;
                    if (session.state().revision() != session.state().checkpointRevision()) {
                        throw problem("VERSION_CHANGED", "checkpoint", "Uncheckpointed edits and a moved Git version require explicit reconciliation", List.of());
                    }
                    importVersion(context, session, "Recover completed architecture version");
                    return null;
                } catch (IOException failure) { throw new java.io.UncheckedIOException(failure); }
            });
        } catch (java.io.UncheckedIOException failure) { throw failure.getCause(); }
    }

    private void importVersion(RepositoryContext context, EditorJournal.Session session, String rationale) throws IOException {
        var repository = repositories.resolveRepository(context);
        String head = head(repository, context.branch());
        if (Objects.equals(head, session.state().checkpointCommit())) return;
        String dsl = source(repository, head);
        if (!dsl.equals(session.state().dsl())) {
            var changes = ArchitectureSemanticPatch.between(session.state().dsl(), dsl);
            String id = UUID.randomUUID().toString();
            session.append(new Metadata(id, id, id, rationale), context.username(), "VERSION_IMPORT", null,
                    EditorJournal.hash("version:" + head), dsl, List.copyOf(affected(changes)));
        }
        session.adoptVersion(head, context.username(), rationale);
    }

    private Change transform(RepositoryContext context, Command command, String current, List<OperationSummary> operations,
                             java.util.function.Function<String, Entry> loadOperation) {
        if (command.operation() instanceof SemanticCommand semantic) return transformer.apply(current, semantic.command());
        String target = target(command.operation());
        OperationSummary selected = operations.stream().filter(e -> e.commandId().equals(target) && e.actor().equals(context.username()))
                .findFirst().orElseThrow(() -> problem("NOT_FOUND", "targetOperationId", "No personal operation in this workspace", List.of()));
        boolean redo = command.operation() instanceof RedoArchitectureCommand;
        if (selected.bodyVersion() != 1 || (redo && !"UNDO".equals(selected.kind())) || (!redo && "UNDO".equals(selected.kind()))
                || selected.kind().startsWith("VERSION_")) {
            throw problem("NOT_UNDOABLE", "targetOperationId", "Choose an accepted edit for undo or an undo operation for redo", List.of());
        }
        for (OperationSummary newer : operations) {
            if (newer.revision() <= selected.revision()) continue;
            if (target.equals(newer.targetOperationId())) throw problem("ALREADY_INVERTED", "targetOperationId", "This operation already has an inverse", List.of(newer.commandId()));
            if (newer.affectedIds().stream().anyMatch(selected.affectedIds()::contains)) {
                throw problem("UNDO_CONFLICT", "targetOperationId", "A later operation depends on the affected objects", List.of(newer.commandId()));
            }
        }
        Entry body = loadOperation.apply(selected.commandId());
        return transformer.inverse(current, body.beforeDsl(), body.afterDsl());
    }

    private void verifyVersion(RepositoryContext context, State state) throws IOException {
        String actual = head(repositories.resolveRepository(context), context.branch());
        if (!Objects.equals(actual, state.checkpointCommit())) {
            throw problem("VERSION_CHANGED", "checkpoint", "A version action must be reconciled before accepting further edits", List.of());
        }
    }

    private Snapshot snapshot(RepositoryContext context) throws IOException {
        Snapshot snapshot = journal.read(context);
        return snapshot == null ? new Snapshot(seed(context), List.of()) : snapshot;
    }
    private State seed(RepositoryContext context) throws IOException {
        DslGitRepository repository = repositories.resolveRepository(context);
        String head = head(repository, context.branch());
        return new State(source(repository, head), 0, head, 0, null);
    }
    private static HistoryEntry history(OperationSummary entry) {
        return new HistoryEntry(entry.commandId(), entry.commandId(), entry.kind(), entry.targetOperationId(),
                entry.actor(), entry.occurredAt(), entry.rationale(), entry.affectedIds(), entry.previousRevision(), entry.revision());
    }
    private static void expect(State state, long expected) {
        if (state.revision() != expected) throw new RevisionConflict(expected, state.revision());
        if (state.pendingCheckpoint() != null) throw problem("CHECKPOINT_PENDING", "checkpoint", "Retry the pending checkpoint", List.of(state.pendingCheckpoint()));
    }
    private static void requireInitialVersion(State state, Context expected) {
        // Before the first accepted edit there is no durable semantic revision to distinguish Git baselines.
        // After revision zero, checkpoints may advance Git without invalidating a semantic preview.
        if (state.revision() == 0 && !Objects.equals(state.checkpointCommit(), expected.commit())) {
            throw problem("CONTEXT_CHANGED", "commit", "The initial architecture version changed; reload and preview again", List.of());
        }
    }
    private static void requireFingerprint(String expected, String actual) {
        if (!expected.equals(actual)) throw problem("COMMAND_ID_REUSED", "commandId", "Command identity was already used with another payload", List.of());
    }
    static void requireContext(RepositoryContext context, Context requested) {
        if (!Context.of(context, requested.commit(), requested.revision()).equals(requested)) {
            throw problem("CONTEXT_CHANGED", "context", "The repository, workspace, branch, actor or mode changed; reopen the editor", List.of());
        }
        if (requested.revision() < 0) throw new IllegalArgumentException("Revision must be nonnegative");
        if (requested.commit() != null) ObjectId.fromString(requested.commit());
    }
    private static void requireWritable(RepositoryContext context) {
        if (context.scope() != RepositoryScope.WORKSPACE) throw problem("READ_ONLY", "context", "Open a private writable workspace", List.of());
    }
    private static String source(DslGitRepository repository, String commit) throws IOException {
        if (commit == null) return "";
        String source = repository.getDslAtCommit(commit);
        if (source == null) throw problem("NOT_FOUND", "commit", "Commit has no architecture document", List.of());
        return source;
    }
    private static String head(DslGitRepository repository, String branch) throws IOException {
        var ref = repository.getGitRepository().exactRef(Constants.R_HEADS + branch);
        return ref == null ? null : ref.getObjectId().name();
    }
    private static void requireReachable(DslGitRepository repository, String head, String selected) throws IOException {
        try (RevWalk walk = new RevWalk(repository.getGitRepository())) {
            if (head != null && walk.isMergedInto(walk.parseCommit(ObjectId.fromString(selected)), walk.parseCommit(ObjectId.fromString(head)))) return;
        } catch (org.eclipse.jgit.errors.MissingObjectException | org.eclipse.jgit.errors.IncorrectObjectTypeException ignored) {
            // Guessed object IDs cannot expose another repository or branch.
        }
        throw problem("NOT_FOUND", "commit", "Commit is not reachable in the selected context", List.of());
    }
    private static Set<String> affected(List<ArchitectureSemanticPatch.BlockChange> changes) {
        Set<String> ids = new LinkedHashSet<>();
        for (var change : changes) {
            ids.add(change.id());
            if (change.id().startsWith("relation:")) {
                String[] tokens = change.id().substring("relation:".length()).split(" ");
                if (tokens.length == 3) { ids.add("element:" + tokens[0]); ids.add("element:" + tokens[2]); }
            }
        }
        return ids;
    }
    private static String kind(Operation operation) {
        return switch (operation) {
            case SemanticCommand semantic -> semantic.command().getClass().getSimpleName();
            case UndoArchitectureCommand ignored -> "UNDO";
            case RedoArchitectureCommand ignored -> "REDO";
        };
    }
    private static String target(Operation operation) {
        return switch (operation) {
            case UndoArchitectureCommand undo -> undo.targetOperationId().toString();
            case RedoArchitectureCommand redo -> redo.targetOperationId().toString();
            default -> null;
        };
    }
    private static String fingerprint(Command command) {
        List<String> parts = new ArrayList<>(List.of(kind(command.operation()), command.context().repositoryId(),
                command.context().workspaceScopeKey(), command.context().branch(), command.context().actor(), command.context().writeMode(),
                command.metadata().correlationId(), command.metadata().causationId(), command.metadata().rationale()));
        parts.add(Long.toString(command.context().revision()));
        if (command.operation() instanceof SemanticCommand semantic) {
            switch (semantic.command()) {
                case CreateArchitectureElement e -> { parts.add(e.id()); parts.add(e.type()); properties(parts, e.properties()); }
                case UpdateArchitectureElement e -> { parts.add(e.id()); parts.add(e.type()); properties(parts, e.properties()); }
                case DeleteArchitectureElement e -> parts.add(e.id());
                case CreateArchitectureRelation r -> { parts.add(r.relation().id()); parts.add(r.status()); }
                case UpdateArchitectureRelation r -> { parts.add(r.relation().id()); parts.add(r.status()); }
                case DeleteArchitectureRelation r -> parts.add(r.relation().id());
                case MoveOrGroupElement e -> { parts.add(e.id()); parts.add(e.parentId()); }
                case SetExchangeProperties e -> { parts.add(e.objectKind()); parts.add(e.id()); properties(parts, e.properties()); }
                case ClearArchitectureElementProperties e -> { parts.add(e.id()); parts.addAll(e.properties().stream().sorted().toList()); }
                case UpsertArchitectureView e -> { parts.add(e.id()); parts.add(e.title()); parts.add(e.description()); parts.addAll(e.members()); properties(parts, e.properties()); }
                case DeleteArchitectureView e -> parts.add(e.id());
                case StoreExchangeEvidence e -> { parts.add(e.id()); parts.add(e.profile()); parts.add(e.profileVersion()); parts.add(e.fingerprint()); parts.add(e.source()); }
            }
        } else parts.add(target(command.operation()));
        return digest(parts);
    }

    private static String checkpointFingerprint(RepositoryContext context, CreateCheckpointCommand command) {
        return digest(Arrays.asList("CHECKPOINT", EditorJournal.scope(context), context.username(),
                Long.toString(command.context().revision()), command.metadata().rationale(),
                command.metadata().correlationId(), command.metadata().causationId()));
    }

    private static void properties(List<String> parts, Map<String, String> properties) {
        new TreeMap<>(properties).forEach((key, value) -> { parts.add(key); parts.add(value); });
    }

    private static String digest(List<String> parts) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            try (DataOutputStream stream = new DataOutputStream(new DigestOutputStream(OutputStream.nullOutputStream(), digest))) {
                for (String part : parts) {
                    if (part == null) { stream.writeInt(-1); continue; }
                    byte[] encoded = part.getBytes(StandardCharsets.UTF_8);
                    stream.writeInt(encoded.length);
                    stream.write(encoded);
                }
            }
            return HexFormat.of().formatHex(digest.digest());
        } catch (IOException | NoSuchAlgorithmException impossible) { throw new IllegalStateException(impossible); }
    }

    private static CommandProblem problem(String code, String field, String message, List<String> dependencies) {
        return new CommandProblem(code, field, message, dependencies);
    }
}