AiAutomationPolicy.java
package com.taxonomy.portfolio.service;
import com.taxonomy.analysis.dto.AiTargetDtos.AiTargetDescriptor;
import com.taxonomy.analysis.service.AiTargetCatalogService;
import com.taxonomy.analysis.service.LlmProvider;
import com.taxonomy.analysis.service.LlmProviderConfig;
import com.taxonomy.portfolio.dto.CopilotDtos.AiAutomationStatus;
import com.taxonomy.portfolio.dto.CopilotDtos.CopilotRunRequest;
import com.taxonomy.portfolio.model.AiCostPolicy;
import com.taxonomy.portfolio.model.AnalysisAutomationProfile;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.List;
import java.util.Locale;
/**
* Central, fail-closed policy for user-triggered Copilot and unattended Autopilot work.
*
* <p>A custom target is never assumed to be free. Autopilot requires an explicit
* provider, an explicit enable flag and the operator declaration {@code UNMETERED}.
* Manual Copilot remains available for any configured addressable target because
* every run is explicitly requested.</p>
*/
@Component
public class AiAutomationPolicy {
private static final int MIN_PASSES = 1;
private static final int MAX_PASSES = 3;
public static final List<String> AUTOMATIC_STEPS = List.of(
"immutable requirement-version analysis",
"taxonomy scoring with reasons",
"relation-aware architecture view",
"gap and pattern analysis",
"architecture recommendation",
"immutable analysis snapshot",
"deterministic solution reuse proposals",
"source-bound product candidates");
public static final List<String> HUMAN_REVIEW_REQUIRED = List.of(
"confirm or reject taxonomy and relation mappings",
"approve organizational responsibility",
"select a solution or product",
"authorize procurement or implementation",
"merge the draft architecture into an approved branch");
private final LlmProviderConfig providerConfig;
private final AiTargetCatalogService targetCatalog;
private final AiCostPolicy costPolicy;
private final AnalysisAutomationProfile copilotProfile;
private final AnalysisAutomationProfile autopilotProfile;
private final int copilotPasses;
private final int autopilotPasses;
private final int maximumArchitectureNodes;
private final boolean autopilotEnabled;
private final boolean runAfterRequirementSave;
private final String autopilotProvider;
private final boolean autopilotSolutions;
private final boolean autopilotProducts;
private final Duration maximumRuntime;
@Autowired
public AiAutomationPolicy(
LlmProviderConfig providerConfig,
AiTargetCatalogService targetCatalog,
@Value("${taxonomy.ai.cost-policy:METERED}") String costPolicy,
@Value("${taxonomy.ai.copilot.profile:FULL}") String copilotProfile,
@Value("${taxonomy.ai.autopilot.profile:EXHAUSTIVE}") String autopilotProfile,
@Value("${taxonomy.ai.copilot.verification-passes:1}") int copilotPasses,
@Value("${taxonomy.ai.autopilot.verification-passes:2}") int autopilotPasses,
@Value("${taxonomy.ai.max-architecture-nodes:50}") int aiMaximumArchitectureNodes,
@Value("${taxonomy.limits.max-architecture-nodes:50}")
int analysisMaximumArchitectureNodes,
@Value("${taxonomy.ai.autopilot.enabled:false}") boolean autopilotEnabled,
@Value("${taxonomy.ai.autopilot.on-requirement-save:true}") boolean runAfterRequirementSave,
@Value("${taxonomy.ai.autopilot.provider:}") String autopilotProvider,
@Value("${taxonomy.ai.autopilot.propose-solutions:true}") boolean autopilotSolutions,
@Value("${taxonomy.ai.autopilot.propose-products:true}") boolean autopilotProducts,
@Value("${taxonomy.ai.maximum-runtime-seconds:1800}") long maximumRuntimeSeconds) {
this.providerConfig = providerConfig;
this.targetCatalog = targetCatalog;
this.costPolicy = enumValue(AiCostPolicy.class, costPolicy, "taxonomy.ai.cost-policy");
this.copilotProfile = enumValue(
AnalysisAutomationProfile.class, copilotProfile, "taxonomy.ai.copilot.profile");
this.autopilotProfile = enumValue(
AnalysisAutomationProfile.class, autopilotProfile, "taxonomy.ai.autopilot.profile");
this.copilotPasses = boundedPasses(copilotPasses, "taxonomy.ai.copilot.verification-passes");
this.autopilotPasses = boundedPasses(
autopilotPasses, "taxonomy.ai.autopilot.verification-passes");
this.maximumArchitectureNodes = Math.min(
positive(aiMaximumArchitectureNodes, "taxonomy.ai.max-architecture-nodes"),
positive(analysisMaximumArchitectureNodes,
"taxonomy.limits.max-architecture-nodes"));
this.autopilotEnabled = autopilotEnabled;
this.runAfterRequirementSave = runAfterRequirementSave;
this.autopilotProvider = normalizeOptional(autopilotProvider);
this.autopilotSolutions = autopilotSolutions;
this.autopilotProducts = autopilotProducts;
this.maximumRuntime = Duration.ofSeconds(Math.max(60L, maximumRuntimeSeconds));
}
/**
* Source-compatible constructor for tests and callers that use one shared
* architecture-node ceiling for AI automation and the analysis pipeline.
*/
public AiAutomationPolicy(
LlmProviderConfig providerConfig,
AiTargetCatalogService targetCatalog,
String costPolicy,
String copilotProfile,
String autopilotProfile,
int copilotPasses,
int autopilotPasses,
int maximumArchitectureNodes,
boolean autopilotEnabled,
boolean runAfterRequirementSave,
String autopilotProvider,
boolean autopilotSolutions,
boolean autopilotProducts,
long maximumRuntimeSeconds) {
this(providerConfig,
targetCatalog,
costPolicy,
copilotProfile,
autopilotProfile,
copilotPasses,
autopilotPasses,
maximumArchitectureNodes,
maximumArchitectureNodes,
autopilotEnabled,
runAfterRequirementSave,
autopilotProvider,
autopilotSolutions,
autopilotProducts,
maximumRuntimeSeconds);
}
public RunSettings manual(CopilotRunRequest request) {
CopilotRunRequest effective = request != null
? request : new CopilotRunRequest(
null, null, null, null, null, null, null, null);
AnalysisAutomationProfile profile = effective.profile() != null
? effective.profile() : copilotProfile;
int passes = effective.verificationPasses() != null
? boundedPasses(effective.verificationPasses(), "verificationPasses")
: defaultPasses(profile, copilotPasses);
AiTargetDescriptor target;
try {
target = targetCatalog.resolve(effective.targetId(), effective.provider());
} catch (IllegalArgumentException exception) {
throw PortfolioException.validation(exception.getMessage());
}
requireTargetReady(target);
int nodes = effective.maxArchitectureNodes() != null
? positive(effective.maxArchitectureNodes(), "maxArchitectureNodes")
: maximumArchitectureNodes;
if (nodes > maximumArchitectureNodes) {
throw PortfolioException.validation(
"maxArchitectureNodes must not exceed the configured limit of "
+ maximumArchitectureNodes);
}
boolean proposeSolutions = effective.proposeSolutions() != null
? effective.proposeSolutions() : profile != AnalysisAutomationProfile.STANDARD;
boolean proposeProducts = effective.proposeProducts() != null
? effective.proposeProducts() : profile != AnalysisAutomationProfile.STANDARD;
return new RunSettings(
false,
profile,
target.provider(),
target,
nodes,
passes,
proposeSolutions,
proposeProducts,
effective.forceRun());
}
public RunSettings autopilot() {
if (!autopilotReady()) {
throw PortfolioException.validation(autopilotReason());
}
AnalysisAutomationProfile profile = autopilotProfile;
AiTargetDescriptor target = targetCatalog.resolve(
null, explicitAutopilotProvider());
return new RunSettings(
true,
profile,
target.provider(),
target,
maximumArchitectureNodes,
defaultPasses(profile, autopilotPasses),
autopilotSolutions && profile != AnalysisAutomationProfile.STANDARD,
autopilotProducts && profile != AnalysisAutomationProfile.STANDARD,
false);
}
public AiAutomationStatus status() {
String active = resolveProvider(null);
boolean manualReady = isProviderReady(active);
String autoProvider = explicitAutopilotProviderOrNull();
boolean autoReady = autopilotReady();
String reason;
if (autoReady && runAfterRequirementSave) {
reason = "Autopilot is explicitly enabled for an UNMETERED configured provider.";
} else if (autoReady) {
reason = "Autopilot is ready, but automatic execution after requirement saves is disabled.";
} else if (!autopilotEnabled) {
reason = "Autopilot is disabled. Set TAXONOMY_AI_AUTOPILOT_ENABLED=true to opt in.";
} else if (costPolicy != AiCostPolicy.UNMETERED) {
reason = "Autopilot requires TAXONOMY_AI_COST_POLICY=UNMETERED.";
} else if (autoProvider == null) {
reason = "Autopilot requires an explicit TAXONOMY_AI_AUTOPILOT_PROVIDER.";
} else {
reason = providerError(autoProvider);
}
return new AiAutomationStatus(
costPolicy,
manualReady,
autopilotEnabled,
autoReady,
runAfterRequirementSave,
active,
autoProvider,
copilotProfile,
autopilotProfile,
defaultPasses(copilotProfile, copilotPasses),
defaultPasses(autopilotProfile, autopilotPasses),
maximumArchitectureNodes,
reason,
AUTOMATIC_STEPS,
HUMAN_REVIEW_REQUIRED);
}
public boolean autopilotReady() {
String provider = explicitAutopilotProviderOrNull();
return autopilotEnabled
&& costPolicy == AiCostPolicy.UNMETERED
&& provider != null
&& isProviderReady(provider);
}
public boolean automaticAfterRequirementSaveReady() {
return runAfterRequirementSave && autopilotReady();
}
public String autopilotReason() {
return status().reason();
}
public AiCostPolicy costPolicy() {
return costPolicy;
}
public Duration maximumRuntime() {
return maximumRuntime;
}
public AiTargetDescriptor targetForProvider(String provider) {
return targetCatalog.describeProvider(provider);
}
private String resolveProvider(String requested) {
String normalized = normalizeOptional(requested);
if (normalized != null) {
return normalized.toUpperCase(Locale.ROOT);
}
if (providerConfig.isMockMode()) {
return "MOCK";
}
return providerConfig.getActiveProvider().name();
}
private String explicitAutopilotProvider() {
String provider = explicitAutopilotProviderOrNull();
if (provider == null) {
throw PortfolioException.validation(
"Autopilot requires an explicit TAXONOMY_AI_AUTOPILOT_PROVIDER.");
}
return provider;
}
private String explicitAutopilotProviderOrNull() {
return autopilotProvider != null
? autopilotProvider.toUpperCase(Locale.ROOT) : null;
}
private boolean isProviderReady(String provider) {
if (provider == null) {
return false;
}
if ("MOCK".equals(provider)) {
return providerConfig.isMockMode();
}
try {
return providerConfig.isProviderConfigured(LlmProvider.valueOf(provider));
} catch (IllegalArgumentException exception) {
return false;
}
}
private void requireTargetReady(AiTargetDescriptor target) {
if (!target.available()) {
throw PortfolioException.validation(target.unavailableReason() != null
? target.unavailableReason()
: "The selected AI target is unavailable: " + target.targetId());
}
}
private String providerError(String provider) {
if (provider == null) {
return "No AI provider was selected.";
}
if ("MOCK".equals(provider)) {
return "MOCK is available only when llm.mock=true.";
}
try {
String error = providerConfig.getProviderConfigurationError(
LlmProvider.valueOf(provider));
return error != null ? error : "The selected provider is not ready: " + provider;
} catch (IllegalArgumentException exception) {
return "Unknown AI provider: " + provider;
}
}
private static int defaultPasses(AnalysisAutomationProfile profile, int configured) {
if (profile == AnalysisAutomationProfile.STANDARD) return 1;
if (profile == AnalysisAutomationProfile.FULL) return Math.max(1, configured);
return Math.max(2, configured);
}
private static int boundedPasses(int value, String field) {
if (value < MIN_PASSES || value > MAX_PASSES) {
throw new IllegalArgumentException(field + " must be between 1 and 3");
}
return value;
}
private static int positive(int value, String field) {
if (value < 1) {
throw PortfolioException.validation(field + " must be at least 1");
}
return value;
}
private static String normalizeOptional(String value) {
return value == null || value.isBlank() ? null : value.strip();
}
private static <E extends Enum<E>> E enumValue(
Class<E> type, String value, String field) {
try {
return Enum.valueOf(type, value.strip().toUpperCase(Locale.ROOT));
} catch (RuntimeException exception) {
throw new IllegalArgumentException(field + " has unsupported value: " + value, exception);
}
}
public record RunSettings(
boolean autopilot,
AnalysisAutomationProfile profile,
String provider,
AiTargetDescriptor target,
int maxArchitectureNodes,
int verificationPasses,
boolean proposeSolutions,
boolean proposeProducts,
boolean force) {
/** Source-compatible constructor for tests and callers that predate target descriptors. */
public RunSettings(
boolean autopilot,
AnalysisAutomationProfile profile,
String provider,
int maxArchitectureNodes,
int verificationPasses,
boolean proposeSolutions,
boolean proposeProducts,
boolean force) {
this(autopilot, profile, provider, null, maxArchitectureNodes,
verificationPasses, proposeSolutions, proposeProducts, force);
}
}
}