diff --git a/CHANGELOG.md b/CHANGELOG.md index 629d16e..130b6ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **MAS FEAT Compliance Module** (Enterprise): Singapore financial services AI governance + - AI System Registry: `masfeat().registerSystem()`, `masfeat().getSystem()`, `masfeat().updateSystem()`, `masfeat().listSystems()`, `masfeat().activateSystem()`, `masfeat().retireSystem()`, `masfeat().getRegistrySummary()` + - 3-Dimensional Risk Rating: Customer Impact × Model Complexity × Human Reliance + - Materiality Classification: High (sum≥12), Medium (sum≥8), Low (sum<8) + - FEAT Assessments: `masfeat().createAssessment()`, `masfeat().getAssessment()`, `masfeat().updateAssessment()`, `masfeat().listAssessments()`, `masfeat().submitAssessment()`, `masfeat().approveAssessment()`, `masfeat().rejectAssessment()` + - Assessment Lifecycle: pending → in_progress → completed → approved/rejected + - Kill Switch: `masfeat().getKillSwitch()`, `masfeat().configureKillSwitch()`, `masfeat().checkKillSwitch()`, `masfeat().triggerKillSwitch()`, `masfeat().restoreKillSwitch()`, `masfeat().enableKillSwitch()`, `masfeat().disableKillSwitch()`, `masfeat().getKillSwitchHistory()` + - Automatic model shutdown based on accuracy, bias, and error rate thresholds + - New types: `AISystemRegistry`, `AISystemUseCase`, `MaterialityClassification`, `SystemStatus`, `FEATAssessment`, `FEATAssessmentStatus`, `FEATPillar`, `KillSwitch`, `KillSwitchStatus`, `KillSwitchEvent`, `KillSwitchEventType`, `RegistrySummary` + - **proxyLLMCall()**: New primary method for Proxy Mode with improved documentation - Clearly describes Proxy Mode behavior (AxonFlow makes the LLM call on your behalf) - Documents when to use Proxy Mode vs Gateway Mode diff --git a/src/main/java/com/getaxonflow/sdk/AxonFlow.java b/src/main/java/com/getaxonflow/sdk/AxonFlow.java index c37acf7..0a050a5 100644 --- a/src/main/java/com/getaxonflow/sdk/AxonFlow.java +++ b/src/main/java/com/getaxonflow/sdk/AxonFlow.java @@ -21,6 +21,7 @@ import com.getaxonflow.sdk.types.costcontrols.CostControlTypes.*; import com.getaxonflow.sdk.types.executionreplay.ExecutionReplayTypes.*; import com.getaxonflow.sdk.types.policies.PolicyTypes.*; +import com.getaxonflow.sdk.masfeat.MASFEATTypes.*; import com.getaxonflow.sdk.util.*; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; @@ -120,6 +121,7 @@ public final class AxonFlow implements Closeable { private final ResponseCache cache; private final Executor asyncExecutor; private volatile String sessionCookie; // Session cookie for Customer Portal authentication + private final MASFEATNamespace masfeatNamespace; private AxonFlow(AxonFlowConfig config) { this.config = Objects.requireNonNull(config, "config cannot be null"); @@ -128,6 +130,7 @@ private AxonFlow(AxonFlowConfig config) { this.retryExecutor = new RetryExecutor(config.getRetryConfig()); this.cache = new ResponseCache(config.getCacheConfig()); this.asyncExecutor = ForkJoinPool.commonPool(); + this.masfeatNamespace = new MASFEATNamespace(); logger.info("AxonFlow client initialized for {}", config.getEndpoint()); } @@ -215,6 +218,37 @@ public CompletableFuture healthCheckAsync() { return CompletableFuture.supplyAsync(this::healthCheck, asyncExecutor); } + // ======================================================================== + // MAS FEAT Namespace Accessor + // ======================================================================== + + /** + * Returns the MAS FEAT (Monetary Authority of Singapore - Fairness, Ethics, + * Accountability, Transparency) compliance namespace. + * + *

Enterprise Feature: Requires AxonFlow Enterprise license. + * + *

Example usage: + *

{@code
+     * AISystemRegistry system = client.masfeat().registerSystem(
+     *     RegisterSystemRequest.builder()
+     *         .systemId("credit-scoring-ai")
+     *         .systemName("Credit Scoring AI")
+     *         .useCase(AISystemUseCase.CREDIT_SCORING)
+     *         .ownerTeam("Risk Management")
+     *         .customerImpact(4)
+     *         .modelComplexity(3)
+     *         .humanReliance(5)
+     *         .build()
+     * );
+     * }
+ * + * @return the MAS FEAT compliance namespace + */ + public MASFEATNamespace masfeat() { + return masfeatNamespace; + } + /** * Checks if the AxonFlow Orchestrator is healthy. * @@ -3482,6 +3516,777 @@ public CompletableFuture stepGate(workflowId, stepId, request), asyncExecutor); } + // ======================================================================== + // MAS FEAT Namespace Inner Class + // ======================================================================== + + /** + * MAS FEAT (Monetary Authority of Singapore - Fairness, Ethics, Accountability, + * Transparency) compliance namespace. + * + *

Provides methods for AI system registry, FEAT assessments, and kill switch + * management for Singapore financial services compliance. + * + *

Enterprise Feature: Requires AxonFlow Enterprise license. + */ + public final class MASFEATNamespace { + + private static final String BASE_PATH = "/api/v1/masfeat"; + + /** + * Registers a new AI system in the MAS FEAT registry. + * + * @param request the registration request + * @return the registered system + */ + public AISystemRegistry registerSystem(RegisterSystemRequest request) { + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute(() -> { + // Map SDK field names to backend field names + Map body = new HashMap<>(); + body.put("system_id", request.getSystemId()); + body.put("system_name", request.getSystemName()); + if (request.getDescription() != null) { + body.put("description", request.getDescription()); + } + if (request.getUseCase() != null) { + body.put("use_case", request.getUseCase().getValue()); + } + body.put("owner_team", request.getOwnerTeam()); + if (request.getTechnicalOwner() != null) { + body.put("technical_owner", request.getTechnicalOwner()); + } + // businessOwner maps to owner_email + if (request.getBusinessOwner() != null) { + body.put("owner_email", request.getBusinessOwner()); + } + // Risk rating fields + body.put("risk_rating_impact", request.getCustomerImpact()); + body.put("risk_rating_complexity", request.getModelComplexity()); + body.put("risk_rating_reliance", request.getHumanReliance()); + if (request.getMetadata() != null) { + body.put("metadata", request.getMetadata()); + } + + Request httpRequest = buildOrchestratorRequest("POST", BASE_PATH + "/registry", body); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseSystemResponse(response); + } + }, "masfeat.registerSystem"); + } + + /** + * Activates an AI system (changes status to 'active'). + * + * @param systemId the system UUID (not the systemId string) + * @return the activated system + */ + public AISystemRegistry activateSystem(String systemId) { + Objects.requireNonNull(systemId, "systemId cannot be null"); + + return retryExecutor.execute(() -> { + Map body = new HashMap<>(); + body.put("status", "active"); + + Request httpRequest = buildOrchestratorRequest("PUT", BASE_PATH + "/registry/" + systemId, body); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseSystemResponse(response); + } + }, "masfeat.activateSystem"); + } + + /** + * Gets an AI system by its UUID. + * + * @param systemId the system UUID + * @return the system + */ + public AISystemRegistry getSystem(String systemId) { + Objects.requireNonNull(systemId, "systemId cannot be null"); + + return retryExecutor.execute(() -> { + Request httpRequest = buildOrchestratorRequest("GET", BASE_PATH + "/registry/" + systemId, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseSystemResponse(response); + } + }, "masfeat.getSystem"); + } + + /** + * Gets the registry summary statistics. + * + * @return the registry summary + */ + public RegistrySummary getRegistrySummary() { + return retryExecutor.execute(() -> { + Request httpRequest = buildOrchestratorRequest("GET", BASE_PATH + "/registry/summary", null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseSummaryResponse(response); + } + }, "masfeat.getRegistrySummary"); + } + + /** + * Creates a new FEAT assessment. + * + * @param request the assessment creation request + * @return the created assessment + */ + public FEATAssessment createAssessment(CreateAssessmentRequest request) { + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute(() -> { + Map body = new HashMap<>(); + body.put("system_id", request.getSystemId()); + body.put("assessment_type", request.getAssessmentType()); + if (request.getAssessors() != null) { + body.put("assessors", request.getAssessors()); + } + + Request httpRequest = buildOrchestratorRequest("POST", BASE_PATH + "/assessments", body); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseAssessmentResponse(response); + } + }, "masfeat.createAssessment"); + } + + /** + * Gets a FEAT assessment by its ID. + * + * @param assessmentId the assessment ID + * @return the assessment + */ + public FEATAssessment getAssessment(String assessmentId) { + Objects.requireNonNull(assessmentId, "assessmentId cannot be null"); + + return retryExecutor.execute(() -> { + Request httpRequest = buildOrchestratorRequest("GET", BASE_PATH + "/assessments/" + assessmentId, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseAssessmentResponse(response); + } + }, "masfeat.getAssessment"); + } + + /** + * Updates a FEAT assessment with pillar scores and details. + * + * @param assessmentId the assessment ID + * @param request the update request + * @return the updated assessment + */ + public FEATAssessment updateAssessment(String assessmentId, UpdateAssessmentRequest request) { + Objects.requireNonNull(assessmentId, "assessmentId cannot be null"); + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute(() -> { + Map body = new HashMap<>(); + if (request.getFairnessScore() != null) { + body.put("fairness_score", request.getFairnessScore()); + } + if (request.getEthicsScore() != null) { + body.put("ethics_score", request.getEthicsScore()); + } + if (request.getAccountabilityScore() != null) { + body.put("accountability_score", request.getAccountabilityScore()); + } + if (request.getTransparencyScore() != null) { + body.put("transparency_score", request.getTransparencyScore()); + } + if (request.getFairnessDetails() != null) { + body.put("fairness_details", request.getFairnessDetails()); + } + if (request.getEthicsDetails() != null) { + body.put("ethics_details", request.getEthicsDetails()); + } + if (request.getAccountabilityDetails() != null) { + body.put("accountability_details", request.getAccountabilityDetails()); + } + if (request.getTransparencyDetails() != null) { + body.put("transparency_details", request.getTransparencyDetails()); + } + if (request.getFindings() != null) { + body.put("findings", request.getFindings()); + } + if (request.getRecommendations() != null) { + body.put("recommendations", request.getRecommendations()); + } + if (request.getAssessors() != null) { + body.put("assessors", request.getAssessors()); + } + + Request httpRequest = buildOrchestratorRequest("PUT", BASE_PATH + "/assessments/" + assessmentId, body); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseAssessmentResponse(response); + } + }, "masfeat.updateAssessment"); + } + + /** + * Submits a FEAT assessment for review. + * + * @param assessmentId the assessment ID + * @return the submitted assessment + */ + public FEATAssessment submitAssessment(String assessmentId) { + Objects.requireNonNull(assessmentId, "assessmentId cannot be null"); + + return retryExecutor.execute(() -> { + Request httpRequest = buildOrchestratorRequest("POST", BASE_PATH + "/assessments/" + assessmentId + "/submit", null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseAssessmentResponse(response); + } + }, "masfeat.submitAssessment"); + } + + /** + * Approves a FEAT assessment. + * + * @param assessmentId the assessment ID + * @param request the approval request + * @return the approved assessment + */ + public FEATAssessment approveAssessment(String assessmentId, ApproveAssessmentRequest request) { + Objects.requireNonNull(assessmentId, "assessmentId cannot be null"); + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute(() -> { + Map body = new HashMap<>(); + body.put("approved_by", request.getApprovedBy()); + if (request.getComments() != null) { + body.put("comments", request.getComments()); + } + + Request httpRequest = buildOrchestratorRequest("POST", BASE_PATH + "/assessments/" + assessmentId + "/approve", body); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseAssessmentResponse(response); + } + }, "masfeat.approveAssessment"); + } + + /** + * Rejects a FEAT assessment. + * + * @param assessmentId the assessment ID + * @param request the rejection request + * @return the rejected assessment + */ + public FEATAssessment rejectAssessment(String assessmentId, RejectAssessmentRequest request) { + Objects.requireNonNull(assessmentId, "assessmentId cannot be null"); + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute(() -> { + Map body = new HashMap<>(); + body.put("rejected_by", request.getRejectedBy()); + body.put("reason", request.getReason()); + + Request httpRequest = buildOrchestratorRequest("POST", BASE_PATH + "/assessments/" + assessmentId + "/reject", body); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseAssessmentResponse(response); + } + }, "masfeat.rejectAssessment"); + } + + /** + * Gets the kill switch configuration for an AI system. + * + * @param systemId the system ID (string ID, not UUID) + * @return the kill switch configuration + */ + public KillSwitch getKillSwitch(String systemId) { + Objects.requireNonNull(systemId, "systemId cannot be null"); + + return retryExecutor.execute(() -> { + Request httpRequest = buildOrchestratorRequest("GET", BASE_PATH + "/killswitch/" + systemId, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseKillSwitchResponse(response); + } + }, "masfeat.getKillSwitch"); + } + + /** + * Configures the kill switch for an AI system. + * + * @param systemId the system ID (string ID, not UUID) + * @param request the configuration request + * @return the configured kill switch + */ + public KillSwitch configureKillSwitch(String systemId, ConfigureKillSwitchRequest request) { + Objects.requireNonNull(systemId, "systemId cannot be null"); + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute(() -> { + Map body = new HashMap<>(); + if (request.getAccuracyThreshold() != null) { + body.put("accuracy_threshold", request.getAccuracyThreshold()); + } + if (request.getBiasThreshold() != null) { + body.put("bias_threshold", request.getBiasThreshold()); + } + if (request.getErrorRateThreshold() != null) { + body.put("error_rate_threshold", request.getErrorRateThreshold()); + } + if (request.getAutoTriggerEnabled() != null) { + body.put("auto_trigger_enabled", request.getAutoTriggerEnabled()); + } + + // Note: configure uses POST, not PUT + Request httpRequest = buildOrchestratorRequest("POST", BASE_PATH + "/killswitch/" + systemId + "/configure", body); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseKillSwitchResponse(response); + } + }, "masfeat.configureKillSwitch"); + } + + /** + * Triggers the kill switch for an AI system. + * + * @param systemId the system ID (string ID, not UUID) + * @param request the trigger request + * @return the triggered kill switch + */ + public KillSwitch triggerKillSwitch(String systemId, TriggerKillSwitchRequest request) { + Objects.requireNonNull(systemId, "systemId cannot be null"); + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute(() -> { + Map body = new HashMap<>(); + body.put("reason", request.getReason()); + if (request.getTriggeredBy() != null) { + body.put("triggered_by", request.getTriggeredBy()); + } + + Request httpRequest = buildOrchestratorRequest("POST", BASE_PATH + "/killswitch/" + systemId + "/trigger", body); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseKillSwitchResponse(response); + } + }, "masfeat.triggerKillSwitch"); + } + + /** + * Restores the kill switch for an AI system after remediation. + * + * @param systemId the system ID (string ID, not UUID) + * @param request the restore request + * @return the restored kill switch + */ + public KillSwitch restoreKillSwitch(String systemId, RestoreKillSwitchRequest request) { + Objects.requireNonNull(systemId, "systemId cannot be null"); + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute(() -> { + Map body = new HashMap<>(); + body.put("reason", request.getReason()); + if (request.getRestoredBy() != null) { + body.put("restored_by", request.getRestoredBy()); + } + + Request httpRequest = buildOrchestratorRequest("POST", BASE_PATH + "/killswitch/" + systemId + "/restore", body); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseKillSwitchResponse(response); + } + }, "masfeat.restoreKillSwitch"); + } + + /** + * Gets the kill switch event history for an AI system. + * + * @param systemId the system ID (string ID, not UUID) + * @param limit maximum number of events to return + * @return list of kill switch events + */ + public List getKillSwitchHistory(String systemId, int limit) { + Objects.requireNonNull(systemId, "systemId cannot be null"); + + return retryExecutor.execute(() -> { + String path = BASE_PATH + "/killswitch/" + systemId + "/history"; + if (limit > 0) { + path += "?limit=" + limit; + } + + Request httpRequest = buildOrchestratorRequest("GET", path, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseKillSwitchHistoryResponse(response); + } + }, "masfeat.getKillSwitchHistory"); + } + + // ======================================================================== + // Response Parsing Helpers + // ======================================================================== + + private AISystemRegistry parseSystemResponse(Response response) throws IOException { + handleErrorResponse(response); + + ResponseBody body = response.body(); + if (body == null) { + throw new AxonFlowException("Empty response body", response.code(), null); + } + + String json = body.string(); + JsonNode node = objectMapper.readTree(json); + + AISystemRegistry system = new AISystemRegistry(); + system.setId(getTextOrNull(node, "id")); + system.setOrgId(getTextOrNull(node, "org_id")); + system.setSystemId(getTextOrNull(node, "system_id")); + system.setSystemName(getTextOrNull(node, "system_name")); + system.setDescription(getTextOrNull(node, "description")); + system.setOwnerTeam(getTextOrNull(node, "owner_team")); + system.setTechnicalOwner(getTextOrNull(node, "technical_owner")); + system.setBusinessOwner(getTextOrNull(node, "owner_email")); + system.setCreatedBy(getTextOrNull(node, "created_by")); + + // Handle use_case enum + String useCase = getTextOrNull(node, "use_case"); + if (useCase != null) { + try { + system.setUseCase(AISystemUseCase.fromValue(useCase)); + } catch (IllegalArgumentException e) { + logger.warn("Unknown use case: {}", useCase); + } + } + + // Handle risk ratings + system.setCustomerImpact(getIntOrZero(node, "risk_rating_impact")); + system.setModelComplexity(getIntOrZero(node, "risk_rating_complexity")); + system.setHumanReliance(getIntOrZero(node, "risk_rating_reliance")); + + // Handle materiality (may be "materiality" or "materiality_classification") + String materiality = getTextOrNull(node, "materiality"); + if (materiality == null) { + materiality = getTextOrNull(node, "materiality_classification"); + } + if (materiality != null) { + try { + system.setMateriality(MaterialityClassification.fromValue(materiality)); + } catch (IllegalArgumentException e) { + logger.warn("Unknown materiality: {}", materiality); + } + } + + // Handle status + String status = getTextOrNull(node, "status"); + if (status != null) { + try { + system.setStatus(SystemStatus.fromValue(status)); + } catch (IllegalArgumentException e) { + logger.warn("Unknown status: {}", status); + } + } + + // Handle timestamps + system.setCreatedAt(parseInstant(node, "created_at")); + system.setUpdatedAt(parseInstant(node, "updated_at")); + + // Handle metadata + if (node.has("metadata") && !node.get("metadata").isNull()) { + system.setMetadata(objectMapper.convertValue(node.get("metadata"), + new TypeReference>() {})); + } + + return system; + } + + private RegistrySummary parseSummaryResponse(Response response) throws IOException { + handleErrorResponse(response); + + ResponseBody body = response.body(); + if (body == null) { + throw new AxonFlowException("Empty response body", response.code(), null); + } + + String json = body.string(); + JsonNode node = objectMapper.readTree(json); + + RegistrySummary summary = new RegistrySummary(); + summary.setTotalSystems(getIntOrZero(node, "total_systems")); + summary.setActiveSystems(getIntOrZero(node, "active_systems")); + + // Handle high_materiality_count (may be "high_materiality_count" or "high_materiality") + int highMateriality = getIntOrZero(node, "high_materiality_count"); + if (highMateriality == 0) { + highMateriality = getIntOrZero(node, "high_materiality"); + } + summary.setHighMaterialityCount(highMateriality); + + summary.setMediumMaterialityCount(getIntOrZero(node, "medium_materiality_count")); + summary.setLowMaterialityCount(getIntOrZero(node, "low_materiality_count")); + + if (node.has("by_use_case") && !node.get("by_use_case").isNull()) { + summary.setByUseCase(objectMapper.convertValue(node.get("by_use_case"), + new TypeReference>() {})); + } + + if (node.has("by_status") && !node.get("by_status").isNull()) { + summary.setByStatus(objectMapper.convertValue(node.get("by_status"), + new TypeReference>() {})); + } + + return summary; + } + + private FEATAssessment parseAssessmentResponse(Response response) throws IOException { + handleErrorResponse(response); + + ResponseBody body = response.body(); + if (body == null) { + throw new AxonFlowException("Empty response body", response.code(), null); + } + + String json = body.string(); + JsonNode node = objectMapper.readTree(json); + + FEATAssessment assessment = new FEATAssessment(); + assessment.setId(getTextOrNull(node, "id")); + assessment.setOrgId(getTextOrNull(node, "org_id")); + assessment.setSystemId(getTextOrNull(node, "system_id")); + assessment.setAssessmentType(getTextOrNull(node, "assessment_type")); + assessment.setApprovedBy(getTextOrNull(node, "approved_by")); + assessment.setCreatedBy(getTextOrNull(node, "created_by")); + + // Handle status + String status = getTextOrNull(node, "status"); + if (status != null) { + try { + assessment.setStatus(FEATAssessmentStatus.fromValue(status)); + } catch (IllegalArgumentException e) { + logger.warn("Unknown assessment status: {}", status); + } + } + + // Handle scores + assessment.setFairnessScore(getIntegerOrNull(node, "fairness_score")); + assessment.setEthicsScore(getIntegerOrNull(node, "ethics_score")); + assessment.setAccountabilityScore(getIntegerOrNull(node, "accountability_score")); + assessment.setTransparencyScore(getIntegerOrNull(node, "transparency_score")); + + // Overall score may be int or float + if (node.has("overall_score") && !node.get("overall_score").isNull()) { + JsonNode scoreNode = node.get("overall_score"); + if (scoreNode.isNumber()) { + assessment.setOverallScore(scoreNode.asInt()); + } + } + + // Handle timestamps + assessment.setAssessmentDate(parseInstant(node, "assessment_date")); + assessment.setValidUntil(parseInstant(node, "valid_until")); + assessment.setApprovedAt(parseInstant(node, "approved_at")); + assessment.setCreatedAt(parseInstant(node, "created_at")); + assessment.setUpdatedAt(parseInstant(node, "updated_at")); + + // Handle details + if (node.has("fairness_details") && !node.get("fairness_details").isNull()) { + assessment.setFairnessDetails(objectMapper.convertValue(node.get("fairness_details"), + new TypeReference>() {})); + } + if (node.has("ethics_details") && !node.get("ethics_details").isNull()) { + assessment.setEthicsDetails(objectMapper.convertValue(node.get("ethics_details"), + new TypeReference>() {})); + } + if (node.has("accountability_details") && !node.get("accountability_details").isNull()) { + assessment.setAccountabilityDetails(objectMapper.convertValue(node.get("accountability_details"), + new TypeReference>() {})); + } + if (node.has("transparency_details") && !node.get("transparency_details").isNull()) { + assessment.setTransparencyDetails(objectMapper.convertValue(node.get("transparency_details"), + new TypeReference>() {})); + } + + // Handle assessors + if (node.has("assessors") && node.get("assessors").isArray()) { + assessment.setAssessors(objectMapper.convertValue(node.get("assessors"), + new TypeReference>() {})); + } + + // Handle recommendations + if (node.has("recommendations") && node.get("recommendations").isArray()) { + assessment.setRecommendations(objectMapper.convertValue(node.get("recommendations"), + new TypeReference>() {})); + } + + return assessment; + } + + private KillSwitch parseKillSwitchResponse(Response response) throws IOException { + handleErrorResponse(response); + + ResponseBody body = response.body(); + if (body == null) { + throw new AxonFlowException("Empty response body", response.code(), null); + } + + String json = body.string(); + JsonNode node = objectMapper.readTree(json); + + // Handle nested response: {"kill_switch": {...}, "message": "..."} + if (node.has("kill_switch") && !node.get("kill_switch").isNull()) { + node = node.get("kill_switch"); + } + + KillSwitch ks = new KillSwitch(); + ks.setId(getTextOrNull(node, "id")); + ks.setOrgId(getTextOrNull(node, "org_id")); + ks.setSystemId(getTextOrNull(node, "system_id")); + ks.setTriggeredBy(getTextOrNull(node, "triggered_by")); + ks.setRestoredBy(getTextOrNull(node, "restored_by")); + + // Handle triggered_reason (may be "triggered_reason" or "trigger_reason") + String triggeredReason = getTextOrNull(node, "triggered_reason"); + if (triggeredReason == null) { + triggeredReason = getTextOrNull(node, "trigger_reason"); + } + ks.setTriggeredReason(triggeredReason); + + // Handle status + String status = getTextOrNull(node, "status"); + if (status != null) { + try { + ks.setStatus(KillSwitchStatus.fromValue(status)); + } catch (IllegalArgumentException e) { + logger.warn("Unknown kill switch status: {}", status); + } + } + + // Handle auto_trigger + if (node.has("auto_trigger_enabled") && !node.get("auto_trigger_enabled").isNull()) { + ks.setAutoTriggerEnabled(node.get("auto_trigger_enabled").asBoolean()); + } + + // Handle thresholds + ks.setAccuracyThreshold(getDoubleOrNull(node, "accuracy_threshold")); + ks.setBiasThreshold(getDoubleOrNull(node, "bias_threshold")); + ks.setErrorRateThreshold(getDoubleOrNull(node, "error_rate_threshold")); + + // Handle timestamps + ks.setTriggeredAt(parseInstant(node, "triggered_at")); + ks.setRestoredAt(parseInstant(node, "restored_at")); + ks.setCreatedAt(parseInstant(node, "created_at")); + ks.setUpdatedAt(parseInstant(node, "updated_at")); + + return ks; + } + + private List parseKillSwitchHistoryResponse(Response response) throws IOException { + handleErrorResponse(response); + + ResponseBody body = response.body(); + if (body == null) { + throw new AxonFlowException("Empty response body", response.code(), null); + } + + String json = body.string(); + JsonNode node = objectMapper.readTree(json); + + // Handle nested response: {"history": [...]} or direct array + JsonNode eventsNode; + if (node.has("history") && node.get("history").isArray()) { + eventsNode = node.get("history"); + } else if (node.has("events") && node.get("events").isArray()) { + eventsNode = node.get("events"); + } else if (node.isArray()) { + eventsNode = node; + } else { + return new ArrayList<>(); + } + + List events = new ArrayList<>(); + for (JsonNode eventNode : eventsNode) { + KillSwitchEvent event = new KillSwitchEvent(); + event.setId(getTextOrNull(eventNode, "id")); + event.setKillSwitchId(getTextOrNull(eventNode, "kill_switch_id")); + + // Handle event_type (may be "event_type" or "action") + String eventType = getTextOrNull(eventNode, "event_type"); + if (eventType == null) { + eventType = getTextOrNull(eventNode, "action"); + } + event.setEventType(eventType); + + // Handle created_by (may be "created_by" or "performed_by") + String createdBy = getTextOrNull(eventNode, "created_by"); + if (createdBy == null) { + createdBy = getTextOrNull(eventNode, "performed_by"); + } + event.setCreatedBy(createdBy); + + // Handle created_at (may be "created_at" or "performed_at") + java.time.Instant createdAt = parseInstant(eventNode, "created_at"); + if (createdAt == null) { + createdAt = parseInstant(eventNode, "performed_at"); + } + event.setCreatedAt(createdAt); + + // Handle event_data + if (eventNode.has("event_data") && !eventNode.get("event_data").isNull()) { + event.setEventData(objectMapper.convertValue(eventNode.get("event_data"), + new TypeReference>() {})); + } else { + // Build event_data from individual fields if present + Map eventData = new HashMap<>(); + String prevStatus = getTextOrNull(eventNode, "previous_status"); + String newStatus = getTextOrNull(eventNode, "new_status"); + String reason = getTextOrNull(eventNode, "reason"); + if (prevStatus != null) eventData.put("previous_status", prevStatus); + if (newStatus != null) eventData.put("new_status", newStatus); + if (reason != null) eventData.put("reason", reason); + if (!eventData.isEmpty()) { + event.setEventData(eventData); + } + } + + events.add(event); + } + + return events; + } + + // ======================================================================== + // JSON Helper Methods + // ======================================================================== + + private String getTextOrNull(JsonNode node, String field) { + if (node.has(field) && !node.get(field).isNull()) { + return node.get(field).asText(); + } + return null; + } + + private int getIntOrZero(JsonNode node, String field) { + if (node.has(field) && !node.get(field).isNull()) { + return node.get(field).asInt(); + } + return 0; + } + + private Integer getIntegerOrNull(JsonNode node, String field) { + if (node.has(field) && !node.get(field).isNull()) { + return node.get(field).asInt(); + } + return null; + } + + private Double getDoubleOrNull(JsonNode node, String field) { + if (node.has(field) && !node.get(field).isNull()) { + return node.get(field).asDouble(); + } + return null; + } + + private java.time.Instant parseInstant(JsonNode node, String field) { + if (node.has(field) && !node.get(field).isNull()) { + String value = node.get(field).asText(); + try { + return java.time.Instant.parse(value); + } catch (Exception e) { + logger.warn("Failed to parse timestamp '{}': {}", value, e.getMessage()); + } + } + return null; + } + } + @Override public void close() { httpClient.dispatcher().executorService().shutdown(); diff --git a/src/main/java/com/getaxonflow/sdk/masfeat/MASFEATTypes.java b/src/main/java/com/getaxonflow/sdk/masfeat/MASFEATTypes.java new file mode 100644 index 0000000..70aa3ca --- /dev/null +++ b/src/main/java/com/getaxonflow/sdk/masfeat/MASFEATTypes.java @@ -0,0 +1,943 @@ +package com.getaxonflow.sdk.masfeat; + +import java.time.Instant; +import java.util.List; +import java.util.Map; + +/** + * MAS FEAT Compliance Types for Singapore Regulatory Compliance. + * + *

This class contains all types for the MAS FEAT (Monetary Authority of Singapore - + * Fairness, Ethics, Accountability, Transparency) compliance module. + * + *

Enterprise Feature: Requires AxonFlow Enterprise license. + */ +public final class MASFEATTypes { + + private MASFEATTypes() { + // Utility class + } + + // ========================================================================= + // Enums + // ========================================================================= + + /** Materiality classification based on 3D risk rating. */ + public enum MaterialityClassification { + HIGH("high"), + MEDIUM("medium"), + LOW("low"); + + private final String value; + + MaterialityClassification(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static MaterialityClassification fromValue(String value) { + for (MaterialityClassification e : values()) { + if (e.value.equals(value)) { + return e; + } + } + throw new IllegalArgumentException("Unknown materiality: " + value); + } + } + + /** AI system lifecycle status. */ + public enum SystemStatus { + DRAFT("draft"), + ACTIVE("active"), + SUSPENDED("suspended"), + RETIRED("retired"); + + private final String value; + + SystemStatus(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static SystemStatus fromValue(String value) { + for (SystemStatus e : values()) { + if (e.value.equals(value)) { + return e; + } + } + throw new IllegalArgumentException("Unknown status: " + value); + } + } + + /** FEAT assessment lifecycle status. */ + public enum FEATAssessmentStatus { + PENDING("pending"), + IN_PROGRESS("in_progress"), + COMPLETED("completed"), + APPROVED("approved"), + REJECTED("rejected"); + + private final String value; + + FEATAssessmentStatus(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static FEATAssessmentStatus fromValue(String value) { + for (FEATAssessmentStatus e : values()) { + if (e.value.equals(value)) { + return e; + } + } + throw new IllegalArgumentException("Unknown assessment status: " + value); + } + } + + /** Kill switch status. */ + public enum KillSwitchStatus { + ENABLED("enabled"), + DISABLED("disabled"), + TRIGGERED("triggered"); + + private final String value; + + KillSwitchStatus(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static KillSwitchStatus fromValue(String value) { + for (KillSwitchStatus e : values()) { + if (e.value.equals(value)) { + return e; + } + } + throw new IllegalArgumentException("Unknown kill switch status: " + value); + } + } + + /** AI system use case categories. */ + public enum AISystemUseCase { + CREDIT_SCORING("credit_scoring"), + ROBO_ADVISORY("robo_advisory"), + INSURANCE_UNDERWRITING("insurance_underwriting"), + TRADING_ALGORITHM("trading_algorithm"), + AML_CFT("aml_cft"), + CUSTOMER_SERVICE("customer_service"), + FRAUD_DETECTION("fraud_detection"), + OTHER("other"); + + private final String value; + + AISystemUseCase(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static AISystemUseCase fromValue(String value) { + for (AISystemUseCase e : values()) { + if (e.value.equals(value)) { + return e; + } + } + throw new IllegalArgumentException("Unknown use case: " + value); + } + } + + /** FEAT framework pillars. */ + public enum FEATPillar { + FAIRNESS("fairness"), + ETHICS("ethics"), + ACCOUNTABILITY("accountability"), + TRANSPARENCY("transparency"); + + private final String value; + + FEATPillar(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static FEATPillar fromValue(String value) { + for (FEATPillar e : values()) { + if (e.value.equals(value)) { + return e; + } + } + throw new IllegalArgumentException("Unknown pillar: " + value); + } + } + + /** FEAT assessment finding severity. */ + public enum FindingSeverity { + CRITICAL("critical"), + MAJOR("major"), + MINOR("minor"), + OBSERVATION("observation"); + + private final String value; + + FindingSeverity(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static FindingSeverity fromValue(String value) { + for (FindingSeverity e : values()) { + if (e.value.equals(value)) { + return e; + } + } + throw new IllegalArgumentException("Unknown finding severity: " + value); + } + } + + /** FEAT assessment finding status. */ + public enum FindingStatus { + OPEN("open"), + RESOLVED("resolved"), + ACCEPTED("accepted"); + + private final String value; + + FindingStatus(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static FindingStatus fromValue(String value) { + for (FindingStatus e : values()) { + if (e.value.equals(value)) { + return e; + } + } + throw new IllegalArgumentException("Unknown finding status: " + value); + } + } + + // ========================================================================= + // Finding Type + // ========================================================================= + + /** A FEAT assessment finding. */ + public static class Finding { + private String id; + private FEATPillar pillar; + private FindingSeverity severity; + private String category; + private String description; + private FindingStatus status; + private String remediation; + private Instant dueDate; + + public Finding() {} + + private Finding(Builder builder) { + this.id = builder.id; + this.pillar = builder.pillar; + this.severity = builder.severity; + this.category = builder.category; + this.description = builder.description; + this.status = builder.status; + this.remediation = builder.remediation; + this.dueDate = builder.dueDate; + } + + public static Builder builder() { + return new Builder(); + } + + public String getId() { return id; } + public void setId(String id) { this.id = id; } + public FEATPillar getPillar() { return pillar; } + public void setPillar(FEATPillar pillar) { this.pillar = pillar; } + public FindingSeverity getSeverity() { return severity; } + public void setSeverity(FindingSeverity severity) { this.severity = severity; } + public String getCategory() { return category; } + public void setCategory(String category) { this.category = category; } + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + public FindingStatus getStatus() { return status; } + public void setStatus(FindingStatus status) { this.status = status; } + public String getRemediation() { return remediation; } + public void setRemediation(String remediation) { this.remediation = remediation; } + public Instant getDueDate() { return dueDate; } + public void setDueDate(Instant dueDate) { this.dueDate = dueDate; } + + public static class Builder { + private String id; + private FEATPillar pillar; + private FindingSeverity severity; + private String category; + private String description; + private FindingStatus status; + private String remediation; + private Instant dueDate; + + public Builder id(String id) { this.id = id; return this; } + public Builder pillar(FEATPillar pillar) { this.pillar = pillar; return this; } + public Builder severity(FindingSeverity severity) { this.severity = severity; return this; } + public Builder category(String category) { this.category = category; return this; } + public Builder description(String description) { this.description = description; return this; } + public Builder status(FindingStatus status) { this.status = status; return this; } + public Builder remediation(String remediation) { this.remediation = remediation; return this; } + public Builder dueDate(Instant dueDate) { this.dueDate = dueDate; return this; } + + public Finding build() { + return new Finding(this); + } + } + } + + // ========================================================================= + // AI System Registry Types + // ========================================================================= + + /** Request to register an AI system. */ + public static class RegisterSystemRequest { + private final String systemId; + private final String systemName; + private final AISystemUseCase useCase; + private final String ownerTeam; + private final int customerImpact; + private final int modelComplexity; + private final int humanReliance; + private String description; + private String technicalOwner; + private String businessOwner; + private Map metadata; + + private RegisterSystemRequest(Builder builder) { + this.systemId = builder.systemId; + this.systemName = builder.systemName; + this.useCase = builder.useCase; + this.ownerTeam = builder.ownerTeam; + this.customerImpact = builder.customerImpact; + this.modelComplexity = builder.modelComplexity; + this.humanReliance = builder.humanReliance; + this.description = builder.description; + this.technicalOwner = builder.technicalOwner; + this.businessOwner = builder.businessOwner; + this.metadata = builder.metadata; + } + + public static Builder builder() { + return new Builder(); + } + + public String getSystemId() { return systemId; } + public String getSystemName() { return systemName; } + public AISystemUseCase getUseCase() { return useCase; } + public String getOwnerTeam() { return ownerTeam; } + public int getCustomerImpact() { return customerImpact; } + public int getModelComplexity() { return modelComplexity; } + public int getHumanReliance() { return humanReliance; } + public String getDescription() { return description; } + public String getTechnicalOwner() { return technicalOwner; } + public String getBusinessOwner() { return businessOwner; } + public Map getMetadata() { return metadata; } + + public static class Builder { + private String systemId; + private String systemName; + private AISystemUseCase useCase; + private String ownerTeam; + private int customerImpact; + private int modelComplexity; + private int humanReliance; + private String description; + private String technicalOwner; + private String businessOwner; + private Map metadata; + + public Builder systemId(String systemId) { this.systemId = systemId; return this; } + public Builder systemName(String systemName) { this.systemName = systemName; return this; } + public Builder useCase(AISystemUseCase useCase) { this.useCase = useCase; return this; } + public Builder ownerTeam(String ownerTeam) { this.ownerTeam = ownerTeam; return this; } + public Builder customerImpact(int customerImpact) { this.customerImpact = customerImpact; return this; } + public Builder modelComplexity(int modelComplexity) { this.modelComplexity = modelComplexity; return this; } + public Builder humanReliance(int humanReliance) { this.humanReliance = humanReliance; return this; } + public Builder description(String description) { this.description = description; return this; } + public Builder technicalOwner(String technicalOwner) { this.technicalOwner = technicalOwner; return this; } + public Builder businessOwner(String businessOwner) { this.businessOwner = businessOwner; return this; } + public Builder metadata(Map metadata) { this.metadata = metadata; return this; } + + public RegisterSystemRequest build() { + return new RegisterSystemRequest(this); + } + } + } + + /** AI system registry entry. */ + public static class AISystemRegistry { + private String id; + private String orgId; + private String systemId; + private String systemName; + private AISystemUseCase useCase; + private String ownerTeam; + private int customerImpact; + private int modelComplexity; + private int humanReliance; + private MaterialityClassification materiality; + private SystemStatus status; + private Instant createdAt; + private Instant updatedAt; + private String description; + private String technicalOwner; + private String businessOwner; + private Map metadata; + private String createdBy; + + public String getId() { return id; } + public void setId(String id) { this.id = id; } + public String getOrgId() { return orgId; } + public void setOrgId(String orgId) { this.orgId = orgId; } + public String getSystemId() { return systemId; } + public void setSystemId(String systemId) { this.systemId = systemId; } + public String getSystemName() { return systemName; } + public void setSystemName(String systemName) { this.systemName = systemName; } + public AISystemUseCase getUseCase() { return useCase; } + public void setUseCase(AISystemUseCase useCase) { this.useCase = useCase; } + public String getOwnerTeam() { return ownerTeam; } + public void setOwnerTeam(String ownerTeam) { this.ownerTeam = ownerTeam; } + public int getCustomerImpact() { return customerImpact; } + public void setCustomerImpact(int customerImpact) { this.customerImpact = customerImpact; } + public int getModelComplexity() { return modelComplexity; } + public void setModelComplexity(int modelComplexity) { this.modelComplexity = modelComplexity; } + public int getHumanReliance() { return humanReliance; } + public void setHumanReliance(int humanReliance) { this.humanReliance = humanReliance; } + public MaterialityClassification getMateriality() { return materiality; } + public void setMateriality(MaterialityClassification materiality) { this.materiality = materiality; } + public SystemStatus getStatus() { return status; } + public void setStatus(SystemStatus status) { this.status = status; } + public Instant getCreatedAt() { return createdAt; } + public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; } + public Instant getUpdatedAt() { return updatedAt; } + public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; } + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + public String getTechnicalOwner() { return technicalOwner; } + public void setTechnicalOwner(String technicalOwner) { this.technicalOwner = technicalOwner; } + public String getBusinessOwner() { return businessOwner; } + public void setBusinessOwner(String businessOwner) { this.businessOwner = businessOwner; } + public Map getMetadata() { return metadata; } + public void setMetadata(Map metadata) { this.metadata = metadata; } + public String getCreatedBy() { return createdBy; } + public void setCreatedBy(String createdBy) { this.createdBy = createdBy; } + } + + /** Registry summary statistics. */ + public static class RegistrySummary { + private int totalSystems; + private int activeSystems; + private int highMaterialityCount; + private int mediumMaterialityCount; + private int lowMaterialityCount; + private Map byUseCase; + private Map byStatus; + + public int getTotalSystems() { return totalSystems; } + public void setTotalSystems(int totalSystems) { this.totalSystems = totalSystems; } + public int getActiveSystems() { return activeSystems; } + public void setActiveSystems(int activeSystems) { this.activeSystems = activeSystems; } + public int getHighMaterialityCount() { return highMaterialityCount; } + public void setHighMaterialityCount(int highMaterialityCount) { this.highMaterialityCount = highMaterialityCount; } + public int getMediumMaterialityCount() { return mediumMaterialityCount; } + public void setMediumMaterialityCount(int mediumMaterialityCount) { this.mediumMaterialityCount = mediumMaterialityCount; } + public int getLowMaterialityCount() { return lowMaterialityCount; } + public void setLowMaterialityCount(int lowMaterialityCount) { this.lowMaterialityCount = lowMaterialityCount; } + public Map getByUseCase() { return byUseCase; } + public void setByUseCase(Map byUseCase) { this.byUseCase = byUseCase; } + public Map getByStatus() { return byStatus; } + public void setByStatus(Map byStatus) { this.byStatus = byStatus; } + } + + // ========================================================================= + // FEAT Assessment Types + // ========================================================================= + + /** Request to create a FEAT assessment. */ + public static class CreateAssessmentRequest { + private final String systemId; + private String assessmentType = "initial"; + private List assessors; + + private CreateAssessmentRequest(Builder builder) { + this.systemId = builder.systemId; + this.assessmentType = builder.assessmentType; + this.assessors = builder.assessors; + } + + public static Builder builder() { + return new Builder(); + } + + public String getSystemId() { return systemId; } + public String getAssessmentType() { return assessmentType; } + public List getAssessors() { return assessors; } + + public static class Builder { + private String systemId; + private String assessmentType = "initial"; + private List assessors; + + public Builder systemId(String systemId) { this.systemId = systemId; return this; } + public Builder assessmentType(String assessmentType) { this.assessmentType = assessmentType; return this; } + public Builder assessors(List assessors) { this.assessors = assessors; return this; } + + public CreateAssessmentRequest build() { + return new CreateAssessmentRequest(this); + } + } + } + + /** Request to update a FEAT assessment. */ + public static class UpdateAssessmentRequest { + private Integer fairnessScore; + private Integer ethicsScore; + private Integer accountabilityScore; + private Integer transparencyScore; + private Map fairnessDetails; + private Map ethicsDetails; + private Map accountabilityDetails; + private Map transparencyDetails; + private List findings; + private List recommendations; + private List assessors; + + private UpdateAssessmentRequest(Builder builder) { + this.fairnessScore = builder.fairnessScore; + this.ethicsScore = builder.ethicsScore; + this.accountabilityScore = builder.accountabilityScore; + this.transparencyScore = builder.transparencyScore; + this.fairnessDetails = builder.fairnessDetails; + this.ethicsDetails = builder.ethicsDetails; + this.accountabilityDetails = builder.accountabilityDetails; + this.transparencyDetails = builder.transparencyDetails; + this.findings = builder.findings; + this.recommendations = builder.recommendations; + this.assessors = builder.assessors; + } + + public static Builder builder() { + return new Builder(); + } + + public Integer getFairnessScore() { return fairnessScore; } + public Integer getEthicsScore() { return ethicsScore; } + public Integer getAccountabilityScore() { return accountabilityScore; } + public Integer getTransparencyScore() { return transparencyScore; } + public Map getFairnessDetails() { return fairnessDetails; } + public Map getEthicsDetails() { return ethicsDetails; } + public Map getAccountabilityDetails() { return accountabilityDetails; } + public Map getTransparencyDetails() { return transparencyDetails; } + public List getFindings() { return findings; } + public List getRecommendations() { return recommendations; } + public List getAssessors() { return assessors; } + + public static class Builder { + private Integer fairnessScore; + private Integer ethicsScore; + private Integer accountabilityScore; + private Integer transparencyScore; + private Map fairnessDetails; + private Map ethicsDetails; + private Map accountabilityDetails; + private Map transparencyDetails; + private List findings; + private List recommendations; + private List assessors; + + public Builder fairnessScore(int score) { this.fairnessScore = score; return this; } + public Builder ethicsScore(int score) { this.ethicsScore = score; return this; } + public Builder accountabilityScore(int score) { this.accountabilityScore = score; return this; } + public Builder transparencyScore(int score) { this.transparencyScore = score; return this; } + public Builder fairnessDetails(Map details) { this.fairnessDetails = details; return this; } + public Builder ethicsDetails(Map details) { this.ethicsDetails = details; return this; } + public Builder accountabilityDetails(Map details) { this.accountabilityDetails = details; return this; } + public Builder transparencyDetails(Map details) { this.transparencyDetails = details; return this; } + public Builder findings(List findings) { this.findings = findings; return this; } + public Builder recommendations(List recommendations) { this.recommendations = recommendations; return this; } + public Builder assessors(List assessors) { this.assessors = assessors; return this; } + + public UpdateAssessmentRequest build() { + return new UpdateAssessmentRequest(this); + } + } + } + + /** FEAT assessment record. */ + public static class FEATAssessment { + private String id; + private String orgId; + private String systemId; + private String assessmentType; + private FEATAssessmentStatus status; + private Instant assessmentDate; + private Instant validUntil; + private Integer fairnessScore; + private Integer ethicsScore; + private Integer accountabilityScore; + private Integer transparencyScore; + private Integer overallScore; + private Map fairnessDetails; + private Map ethicsDetails; + private Map accountabilityDetails; + private Map transparencyDetails; + private List findings; + private List recommendations; + private List assessors; + private String approvedBy; + private Instant approvedAt; + private Instant createdAt; + private Instant updatedAt; + private String createdBy; + + public String getId() { return id; } + public void setId(String id) { this.id = id; } + public String getOrgId() { return orgId; } + public void setOrgId(String orgId) { this.orgId = orgId; } + public String getSystemId() { return systemId; } + public void setSystemId(String systemId) { this.systemId = systemId; } + public String getAssessmentType() { return assessmentType; } + public void setAssessmentType(String assessmentType) { this.assessmentType = assessmentType; } + public FEATAssessmentStatus getStatus() { return status; } + public void setStatus(FEATAssessmentStatus status) { this.status = status; } + public Instant getAssessmentDate() { return assessmentDate; } + public void setAssessmentDate(Instant assessmentDate) { this.assessmentDate = assessmentDate; } + public Instant getValidUntil() { return validUntil; } + public void setValidUntil(Instant validUntil) { this.validUntil = validUntil; } + public Integer getFairnessScore() { return fairnessScore; } + public void setFairnessScore(Integer fairnessScore) { this.fairnessScore = fairnessScore; } + public Integer getEthicsScore() { return ethicsScore; } + public void setEthicsScore(Integer ethicsScore) { this.ethicsScore = ethicsScore; } + public Integer getAccountabilityScore() { return accountabilityScore; } + public void setAccountabilityScore(Integer accountabilityScore) { this.accountabilityScore = accountabilityScore; } + public Integer getTransparencyScore() { return transparencyScore; } + public void setTransparencyScore(Integer transparencyScore) { this.transparencyScore = transparencyScore; } + public Integer getOverallScore() { return overallScore; } + public void setOverallScore(Integer overallScore) { this.overallScore = overallScore; } + public Map getFairnessDetails() { return fairnessDetails; } + public void setFairnessDetails(Map fairnessDetails) { this.fairnessDetails = fairnessDetails; } + public Map getEthicsDetails() { return ethicsDetails; } + public void setEthicsDetails(Map ethicsDetails) { this.ethicsDetails = ethicsDetails; } + public Map getAccountabilityDetails() { return accountabilityDetails; } + public void setAccountabilityDetails(Map accountabilityDetails) { this.accountabilityDetails = accountabilityDetails; } + public Map getTransparencyDetails() { return transparencyDetails; } + public void setTransparencyDetails(Map transparencyDetails) { this.transparencyDetails = transparencyDetails; } + public List getFindings() { return findings; } + public void setFindings(List findings) { this.findings = findings; } + public List getRecommendations() { return recommendations; } + public void setRecommendations(List recommendations) { this.recommendations = recommendations; } + public List getAssessors() { return assessors; } + public void setAssessors(List assessors) { this.assessors = assessors; } + public String getApprovedBy() { return approvedBy; } + public void setApprovedBy(String approvedBy) { this.approvedBy = approvedBy; } + public Instant getApprovedAt() { return approvedAt; } + public void setApprovedAt(Instant approvedAt) { this.approvedAt = approvedAt; } + public Instant getCreatedAt() { return createdAt; } + public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; } + public Instant getUpdatedAt() { return updatedAt; } + public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; } + public String getCreatedBy() { return createdBy; } + public void setCreatedBy(String createdBy) { this.createdBy = createdBy; } + } + + /** Request to approve an assessment. */ + public static class ApproveAssessmentRequest { + private final String approvedBy; + private String comments; + + private ApproveAssessmentRequest(Builder builder) { + this.approvedBy = builder.approvedBy; + this.comments = builder.comments; + } + + public static Builder builder() { + return new Builder(); + } + + public String getApprovedBy() { return approvedBy; } + public String getComments() { return comments; } + + public static class Builder { + private String approvedBy; + private String comments; + + public Builder approvedBy(String approvedBy) { this.approvedBy = approvedBy; return this; } + public Builder comments(String comments) { this.comments = comments; return this; } + + public ApproveAssessmentRequest build() { + return new ApproveAssessmentRequest(this); + } + } + } + + /** Request to reject an assessment. */ + public static class RejectAssessmentRequest { + private final String rejectedBy; + private final String reason; + + private RejectAssessmentRequest(Builder builder) { + this.rejectedBy = builder.rejectedBy; + this.reason = builder.reason; + } + + public static Builder builder() { + return new Builder(); + } + + public String getRejectedBy() { return rejectedBy; } + public String getReason() { return reason; } + + public static class Builder { + private String rejectedBy; + private String reason; + + public Builder rejectedBy(String rejectedBy) { this.rejectedBy = rejectedBy; return this; } + public Builder reason(String reason) { this.reason = reason; return this; } + + public RejectAssessmentRequest build() { + return new RejectAssessmentRequest(this); + } + } + } + + // ========================================================================= + // Kill Switch Types + // ========================================================================= + + /** Kill switch configuration. */ + public static class KillSwitch { + private String id; + private String orgId; + private String systemId; + private KillSwitchStatus status; + private boolean autoTriggerEnabled; + private Double accuracyThreshold; + private Double biasThreshold; + private Double errorRateThreshold; + private Instant triggeredAt; + private String triggeredBy; + private String triggeredReason; + private Instant restoredAt; + private String restoredBy; + private Instant createdAt; + private Instant updatedAt; + + public String getId() { return id; } + public void setId(String id) { this.id = id; } + public String getOrgId() { return orgId; } + public void setOrgId(String orgId) { this.orgId = orgId; } + public String getSystemId() { return systemId; } + public void setSystemId(String systemId) { this.systemId = systemId; } + public KillSwitchStatus getStatus() { return status; } + public void setStatus(KillSwitchStatus status) { this.status = status; } + public boolean isAutoTriggerEnabled() { return autoTriggerEnabled; } + public void setAutoTriggerEnabled(boolean autoTriggerEnabled) { this.autoTriggerEnabled = autoTriggerEnabled; } + public Double getAccuracyThreshold() { return accuracyThreshold; } + public void setAccuracyThreshold(Double accuracyThreshold) { this.accuracyThreshold = accuracyThreshold; } + public Double getBiasThreshold() { return biasThreshold; } + public void setBiasThreshold(Double biasThreshold) { this.biasThreshold = biasThreshold; } + public Double getErrorRateThreshold() { return errorRateThreshold; } + public void setErrorRateThreshold(Double errorRateThreshold) { this.errorRateThreshold = errorRateThreshold; } + public Instant getTriggeredAt() { return triggeredAt; } + public void setTriggeredAt(Instant triggeredAt) { this.triggeredAt = triggeredAt; } + public String getTriggeredBy() { return triggeredBy; } + public void setTriggeredBy(String triggeredBy) { this.triggeredBy = triggeredBy; } + public String getTriggeredReason() { return triggeredReason; } + public void setTriggeredReason(String triggeredReason) { this.triggeredReason = triggeredReason; } + public Instant getRestoredAt() { return restoredAt; } + public void setRestoredAt(Instant restoredAt) { this.restoredAt = restoredAt; } + public String getRestoredBy() { return restoredBy; } + public void setRestoredBy(String restoredBy) { this.restoredBy = restoredBy; } + public Instant getCreatedAt() { return createdAt; } + public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; } + public Instant getUpdatedAt() { return updatedAt; } + public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; } + } + + /** Request to configure a kill switch. */ + public static class ConfigureKillSwitchRequest { + private Double accuracyThreshold; + private Double biasThreshold; + private Double errorRateThreshold; + private Boolean autoTriggerEnabled; + + private ConfigureKillSwitchRequest(Builder builder) { + this.accuracyThreshold = builder.accuracyThreshold; + this.biasThreshold = builder.biasThreshold; + this.errorRateThreshold = builder.errorRateThreshold; + this.autoTriggerEnabled = builder.autoTriggerEnabled; + } + + public static Builder builder() { + return new Builder(); + } + + public Double getAccuracyThreshold() { return accuracyThreshold; } + public Double getBiasThreshold() { return biasThreshold; } + public Double getErrorRateThreshold() { return errorRateThreshold; } + public Boolean getAutoTriggerEnabled() { return autoTriggerEnabled; } + + public static class Builder { + private Double accuracyThreshold; + private Double biasThreshold; + private Double errorRateThreshold; + private Boolean autoTriggerEnabled; + + public Builder accuracyThreshold(double threshold) { this.accuracyThreshold = threshold; return this; } + public Builder biasThreshold(double threshold) { this.biasThreshold = threshold; return this; } + public Builder errorRateThreshold(double threshold) { this.errorRateThreshold = threshold; return this; } + public Builder autoTriggerEnabled(boolean enabled) { this.autoTriggerEnabled = enabled; return this; } + + public ConfigureKillSwitchRequest build() { + return new ConfigureKillSwitchRequest(this); + } + } + } + + /** Request to check kill switch metrics. */ + public static class CheckKillSwitchRequest { + private final double accuracy; + private Double biasScore; + private Double errorRate; + + private CheckKillSwitchRequest(Builder builder) { + this.accuracy = builder.accuracy; + this.biasScore = builder.biasScore; + this.errorRate = builder.errorRate; + } + + public static Builder builder() { + return new Builder(); + } + + public double getAccuracy() { return accuracy; } + public Double getBiasScore() { return biasScore; } + public Double getErrorRate() { return errorRate; } + + public static class Builder { + private double accuracy; + private Double biasScore; + private Double errorRate; + + public Builder accuracy(double accuracy) { this.accuracy = accuracy; return this; } + public Builder biasScore(double biasScore) { this.biasScore = biasScore; return this; } + public Builder errorRate(double errorRate) { this.errorRate = errorRate; return this; } + + public CheckKillSwitchRequest build() { + return new CheckKillSwitchRequest(this); + } + } + } + + /** Request to trigger a kill switch. */ + public static class TriggerKillSwitchRequest { + private final String reason; + private String triggeredBy; + + private TriggerKillSwitchRequest(Builder builder) { + this.reason = builder.reason; + this.triggeredBy = builder.triggeredBy; + } + + public static Builder builder() { + return new Builder(); + } + + public String getReason() { return reason; } + public String getTriggeredBy() { return triggeredBy; } + + public static class Builder { + private String reason; + private String triggeredBy; + + public Builder reason(String reason) { this.reason = reason; return this; } + public Builder triggeredBy(String triggeredBy) { this.triggeredBy = triggeredBy; return this; } + + public TriggerKillSwitchRequest build() { + return new TriggerKillSwitchRequest(this); + } + } + } + + /** Request to restore a kill switch. */ + public static class RestoreKillSwitchRequest { + private final String reason; + private String restoredBy; + + private RestoreKillSwitchRequest(Builder builder) { + this.reason = builder.reason; + this.restoredBy = builder.restoredBy; + } + + public static Builder builder() { + return new Builder(); + } + + public String getReason() { return reason; } + public String getRestoredBy() { return restoredBy; } + + public static class Builder { + private String reason; + private String restoredBy; + + public Builder reason(String reason) { this.reason = reason; return this; } + public Builder restoredBy(String restoredBy) { this.restoredBy = restoredBy; return this; } + + public RestoreKillSwitchRequest build() { + return new RestoreKillSwitchRequest(this); + } + } + } + + /** Kill switch event record. */ + public static class KillSwitchEvent { + private String id; + private String killSwitchId; + private String eventType; + private Map eventData; + private String createdBy; + private Instant createdAt; + + public String getId() { return id; } + public void setId(String id) { this.id = id; } + public String getKillSwitchId() { return killSwitchId; } + public void setKillSwitchId(String killSwitchId) { this.killSwitchId = killSwitchId; } + public String getEventType() { return eventType; } + public void setEventType(String eventType) { this.eventType = eventType; } + public Map getEventData() { return eventData; } + public void setEventData(Map eventData) { this.eventData = eventData; } + public String getCreatedBy() { return createdBy; } + public void setCreatedBy(String createdBy) { this.createdBy = createdBy; } + public Instant getCreatedAt() { return createdAt; } + public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; } + } +} diff --git a/src/main/java/com/getaxonflow/sdk/masfeat/package-info.java b/src/main/java/com/getaxonflow/sdk/masfeat/package-info.java new file mode 100644 index 0000000..d844c6f --- /dev/null +++ b/src/main/java/com/getaxonflow/sdk/masfeat/package-info.java @@ -0,0 +1,50 @@ +/** + * MAS FEAT Compliance module for Singapore regulatory compliance. + * + *

This package provides types and client for the MAS FEAT (Monetary Authority of Singapore - + * Fairness, Ethics, Accountability, Transparency) compliance framework. + * + *

Enterprise Feature: Requires AxonFlow Enterprise license. + * + *

Features

+ * + * + *

Example

+ *
{@code
+ * AxonFlowClient client = AxonFlowClient.builder()
+ *     .apiKey("your-api-key")
+ *     .endpoint("http://localhost:8081")
+ *     .build();
+ *
+ * // Register an AI system
+ * AISystemRegistry system = client.masfeat().registerSystem(
+ *     RegisterSystemRequest.builder()
+ *         .systemId("credit-scoring-ai-v1")
+ *         .systemName("Credit Scoring AI")
+ *         .useCase(AISystemUseCase.CREDIT_SCORING)
+ *         .ownerTeam("Risk Management")
+ *         .customerImpact(4)
+ *         .modelComplexity(3)
+ *         .humanReliance(5)
+ *         .build()
+ * );
+ *
+ * // Configure kill switch
+ * KillSwitch killSwitch = client.masfeat().configureKillSwitch(
+ *     "credit-scoring-ai-v1",
+ *     ConfigureKillSwitchRequest.builder()
+ *         .accuracyThreshold(0.85)
+ *         .biasThreshold(0.15)
+ *         .autoTriggerEnabled(true)
+ *         .build()
+ * );
+ * }
+ * + * @see com.getaxonflow.sdk.masfeat.MASFEATTypes + */ +package com.getaxonflow.sdk.masfeat; diff --git a/src/test/java/com/getaxonflow/sdk/masfeat/MASFEATClientTest.java b/src/test/java/com/getaxonflow/sdk/masfeat/MASFEATClientTest.java new file mode 100644 index 0000000..4e7fe17 --- /dev/null +++ b/src/test/java/com/getaxonflow/sdk/masfeat/MASFEATClientTest.java @@ -0,0 +1,475 @@ +/* + * Copyright 2026 AxonFlow + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + */ +package com.getaxonflow.sdk.masfeat; + +import com.getaxonflow.sdk.AxonFlow; +import com.getaxonflow.sdk.masfeat.MASFEATTypes.*; +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.assertj.core.api.Assertions.*; + +/** + * Tests for MAS FEAT client methods. + */ +@WireMockTest +@DisplayName("MAS FEAT Client Tests") +class MASFEATClientTest { + + private AxonFlow client; + + @BeforeEach + void setUp(WireMockRuntimeInfo wmRuntimeInfo) { + client = AxonFlow.create(AxonFlow.builder() + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("test-client") + .clientSecret("test-secret") + .build()); + } + + @Nested + @DisplayName("Registry Methods") + class RegistryMethodsTest { + + @Test + @DisplayName("Should register a new AI system") + void testRegisterSystem() { + String responseJson = "{" + + "\"id\": \"sys-123\"," + + "\"org_id\": \"org-456\"," + + "\"system_id\": \"credit-model-v1\"," + + "\"system_name\": \"Credit Scoring Model\"," + + "\"use_case\": \"credit_scoring\"," + + "\"owner_team\": \"data-science\"," + + "\"risk_rating_impact\": 3," + + "\"risk_rating_complexity\": 2," + + "\"risk_rating_reliance\": 1," + + "\"materiality\": \"high\"," + + "\"status\": \"draft\"," + + "\"created_at\": \"2026-01-23T12:00:00Z\"," + + "\"updated_at\": \"2026-01-23T12:00:00Z\"" + + "}"; + stubFor(post(urlEqualTo("/api/v1/masfeat/registry")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(responseJson))); + + RegisterSystemRequest request = RegisterSystemRequest.builder() + .systemId("credit-model-v1") + .systemName("Credit Scoring Model") + .useCase(AISystemUseCase.CREDIT_SCORING) + .ownerTeam("data-science") + .customerImpact(3) + .modelComplexity(2) + .humanReliance(1) + .build(); + + AISystemRegistry result = client.masfeat().registerSystem(request); + + assertThat(result.getId()).isEqualTo("sys-123"); + assertThat(result.getSystemName()).isEqualTo("Credit Scoring Model"); + assertThat(result.getMateriality()).isEqualTo(MaterialityClassification.HIGH); + + verify(postRequestedFor(urlEqualTo("/api/v1/masfeat/registry"))); + } + + @Test + @DisplayName("Should get a system by ID") + void testGetSystem() { + String responseJson = "{" + + "\"id\": \"sys-123\"," + + "\"org_id\": \"org-456\"," + + "\"system_id\": \"model-v1\"," + + "\"system_name\": \"Test Model\"," + + "\"use_case\": \"credit_scoring\"," + + "\"owner_team\": \"team\"," + + "\"customer_impact\": 3," + + "\"model_complexity\": 2," + + "\"human_reliance\": 1," + + "\"materiality\": \"high\"," + + "\"status\": \"active\"," + + "\"created_at\": \"2026-01-23T12:00:00Z\"," + + "\"updated_at\": \"2026-01-23T12:00:00Z\"" + + "}"; + stubFor(get(urlEqualTo("/api/v1/masfeat/registry/sys-123")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(responseJson))); + + AISystemRegistry result = client.masfeat().getSystem("sys-123"); + + assertThat(result.getId()).isEqualTo("sys-123"); + assertThat(result.getStatus()).isEqualTo(SystemStatus.ACTIVE); + + verify(getRequestedFor(urlEqualTo("/api/v1/masfeat/registry/sys-123"))); + } + + @Test + @DisplayName("Should activate a system") + void testActivateSystem() { + String responseJson = "{" + + "\"id\": \"sys-123\"," + + "\"org_id\": \"org-456\"," + + "\"system_id\": \"model-v1\"," + + "\"system_name\": \"Test Model\"," + + "\"use_case\": \"credit_scoring\"," + + "\"owner_team\": \"team\"," + + "\"materiality\": \"high\"," + + "\"status\": \"active\"," + + "\"created_at\": \"2026-01-23T12:00:00Z\"," + + "\"updated_at\": \"2026-01-23T12:00:00Z\"" + + "}"; + stubFor(put(urlEqualTo("/api/v1/masfeat/registry/sys-123")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(responseJson))); + + AISystemRegistry result = client.masfeat().activateSystem("sys-123"); + + assertThat(result.getStatus()).isEqualTo(SystemStatus.ACTIVE); + + verify(putRequestedFor(urlEqualTo("/api/v1/masfeat/registry/sys-123"))); + } + + @Test + @DisplayName("Should get registry summary") + void testGetRegistrySummary() { + String responseJson = "{" + + "\"total_systems\": 10," + + "\"active_systems\": 8," + + "\"high_materiality_count\": 2," + + "\"medium_materiality_count\": 5," + + "\"low_materiality_count\": 3" + + "}"; + stubFor(get(urlEqualTo("/api/v1/masfeat/registry/summary")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(responseJson))); + + RegistrySummary result = client.masfeat().getRegistrySummary(); + + assertThat(result.getTotalSystems()).isEqualTo(10); + assertThat(result.getActiveSystems()).isEqualTo(8); + + verify(getRequestedFor(urlEqualTo("/api/v1/masfeat/registry/summary"))); + } + } + + @Nested + @DisplayName("Assessment Methods") + class AssessmentMethodsTest { + + @Test + @DisplayName("Should create a new assessment") + void testCreateAssessment() { + String responseJson = "{" + + "\"id\": \"assess-123\"," + + "\"org_id\": \"org-456\"," + + "\"system_id\": \"sys-789\"," + + "\"assessment_type\": \"annual\"," + + "\"status\": \"pending\"," + + "\"assessment_date\": \"2026-01-23T12:00:00Z\"," + + "\"created_at\": \"2026-01-23T12:00:00Z\"," + + "\"updated_at\": \"2026-01-23T12:00:00Z\"" + + "}"; + stubFor(post(urlEqualTo("/api/v1/masfeat/assessments")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(responseJson))); + + CreateAssessmentRequest request = CreateAssessmentRequest.builder() + .systemId("sys-789") + .assessmentType("annual") + .build(); + + FEATAssessment result = client.masfeat().createAssessment(request); + + assertThat(result.getId()).isEqualTo("assess-123"); + assertThat(result.getStatus()).isEqualTo(FEATAssessmentStatus.PENDING); + + verify(postRequestedFor(urlEqualTo("/api/v1/masfeat/assessments"))); + } + + @Test + @DisplayName("Should get an assessment by ID") + void testGetAssessment() { + String responseJson = "{" + + "\"id\": \"assess-123\"," + + "\"org_id\": \"org-456\"," + + "\"system_id\": \"sys-789\"," + + "\"assessment_type\": \"annual\"," + + "\"status\": \"completed\"," + + "\"assessment_date\": \"2026-01-23T12:00:00Z\"," + + "\"overall_score\": 89," + + "\"created_at\": \"2026-01-23T12:00:00Z\"," + + "\"updated_at\": \"2026-01-23T12:00:00Z\"" + + "}"; + stubFor(get(urlEqualTo("/api/v1/masfeat/assessments/assess-123")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(responseJson))); + + FEATAssessment result = client.masfeat().getAssessment("assess-123"); + + assertThat(result.getId()).isEqualTo("assess-123"); + assertThat(result.getOverallScore()).isEqualTo(89); + + verify(getRequestedFor(urlEqualTo("/api/v1/masfeat/assessments/assess-123"))); + } + + @Test + @DisplayName("Should update an assessment") + void testUpdateAssessment() { + String responseJson = "{" + + "\"id\": \"assess-123\"," + + "\"org_id\": \"org-456\"," + + "\"system_id\": \"sys-789\"," + + "\"assessment_type\": \"annual\"," + + "\"status\": \"in_progress\"," + + "\"assessment_date\": \"2026-01-23T12:00:00Z\"," + + "\"fairness_score\": 85," + + "\"created_at\": \"2026-01-23T12:00:00Z\"," + + "\"updated_at\": \"2026-01-23T12:00:00Z\"" + + "}"; + stubFor(put(urlEqualTo("/api/v1/masfeat/assessments/assess-123")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(responseJson))); + + UpdateAssessmentRequest request = UpdateAssessmentRequest.builder() + .fairnessScore(85) + .build(); + + FEATAssessment result = client.masfeat().updateAssessment("assess-123", request); + + assertThat(result.getFairnessScore()).isEqualTo(85); + + verify(putRequestedFor(urlEqualTo("/api/v1/masfeat/assessments/assess-123"))); + } + + @Test + @DisplayName("Should submit an assessment") + void testSubmitAssessment() { + String responseJson = "{" + + "\"id\": \"assess-123\"," + + "\"org_id\": \"org-456\"," + + "\"system_id\": \"sys-789\"," + + "\"assessment_type\": \"annual\"," + + "\"status\": \"completed\"," + + "\"assessment_date\": \"2026-01-23T12:00:00Z\"," + + "\"created_at\": \"2026-01-23T12:00:00Z\"," + + "\"updated_at\": \"2026-01-23T12:00:00Z\"" + + "}"; + stubFor(post(urlEqualTo("/api/v1/masfeat/assessments/assess-123/submit")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(responseJson))); + + FEATAssessment result = client.masfeat().submitAssessment("assess-123"); + + assertThat(result.getStatus()).isEqualTo(FEATAssessmentStatus.COMPLETED); + + verify(postRequestedFor(urlEqualTo("/api/v1/masfeat/assessments/assess-123/submit"))); + } + + @Test + @DisplayName("Should approve an assessment") + void testApproveAssessment() { + String responseJson = "{" + + "\"id\": \"assess-123\"," + + "\"org_id\": \"org-456\"," + + "\"system_id\": \"sys-789\"," + + "\"assessment_type\": \"annual\"," + + "\"status\": \"approved\"," + + "\"assessment_date\": \"2026-01-23T12:00:00Z\"," + + "\"approved_by\": \"admin@example.com\"," + + "\"approved_at\": \"2026-01-23T13:00:00Z\"," + + "\"created_at\": \"2026-01-23T12:00:00Z\"," + + "\"updated_at\": \"2026-01-23T13:00:00Z\"" + + "}"; + stubFor(post(urlEqualTo("/api/v1/masfeat/assessments/assess-123/approve")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(responseJson))); + + ApproveAssessmentRequest request = ApproveAssessmentRequest.builder().build(); + FEATAssessment result = client.masfeat().approveAssessment("assess-123", request); + + assertThat(result.getStatus()).isEqualTo(FEATAssessmentStatus.APPROVED); + assertThat(result.getApprovedBy()).isEqualTo("admin@example.com"); + + verify(postRequestedFor(urlEqualTo("/api/v1/masfeat/assessments/assess-123/approve"))); + } + } + + @Nested + @DisplayName("Kill Switch Methods") + class KillSwitchMethodsTest { + + @Test + @DisplayName("Should get kill switch status") + void testGetKillSwitch() { + String responseJson = "{" + + "\"id\": \"ks-123\"," + + "\"org_id\": \"org-456\"," + + "\"system_id\": \"sys-789\"," + + "\"status\": \"enabled\"," + + "\"auto_trigger_enabled\": true," + + "\"accuracy_threshold\": 0.95," + + "\"created_at\": \"2026-01-23T12:00:00Z\"," + + "\"updated_at\": \"2026-01-23T12:00:00Z\"" + + "}"; + stubFor(get(urlEqualTo("/api/v1/masfeat/killswitch/sys-789")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(responseJson))); + + KillSwitch result = client.masfeat().getKillSwitch("sys-789"); + + assertThat(result.getId()).isEqualTo("ks-123"); + assertThat(result.getStatus()).isEqualTo(KillSwitchStatus.ENABLED); + assertThat(result.getAccuracyThreshold()).isEqualTo(0.95); + + verify(getRequestedFor(urlEqualTo("/api/v1/masfeat/killswitch/sys-789"))); + } + + @Test + @DisplayName("Should configure kill switch") + void testConfigureKillSwitch() { + String responseJson = "{" + + "\"id\": \"ks-123\"," + + "\"org_id\": \"org-456\"," + + "\"system_id\": \"sys-789\"," + + "\"status\": \"enabled\"," + + "\"auto_trigger_enabled\": true," + + "\"accuracy_threshold\": 0.95," + + "\"created_at\": \"2026-01-23T12:00:00Z\"," + + "\"updated_at\": \"2026-01-23T12:00:00Z\"" + + "}"; + stubFor(post(urlEqualTo("/api/v1/masfeat/killswitch/sys-789/configure")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(responseJson))); + + ConfigureKillSwitchRequest request = ConfigureKillSwitchRequest.builder() + .accuracyThreshold(0.95) + .autoTriggerEnabled(true) + .build(); + + KillSwitch result = client.masfeat().configureKillSwitch("sys-789", request); + + assertThat(result.isAutoTriggerEnabled()).isTrue(); + + verify(postRequestedFor(urlEqualTo("/api/v1/masfeat/killswitch/sys-789/configure"))); + } + + @Test + @DisplayName("Should trigger kill switch") + void testTriggerKillSwitch() { + String responseJson = "{" + + "\"kill_switch\": {" + + "\"id\": \"ks-123\"," + + "\"org_id\": \"org-456\"," + + "\"system_id\": \"sys-789\"," + + "\"status\": \"triggered\"," + + "\"auto_trigger_enabled\": true," + + "\"triggered_reason\": \"Manual trigger\"," + + "\"created_at\": \"2026-01-23T12:00:00Z\"," + + "\"updated_at\": \"2026-01-23T12:00:00Z\"" + + "}," + + "\"message\": \"Kill switch triggered\"" + + "}"; + stubFor(post(urlEqualTo("/api/v1/masfeat/killswitch/sys-789/trigger")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(responseJson))); + + TriggerKillSwitchRequest request = TriggerKillSwitchRequest.builder() + .reason("Manual trigger") + .build(); + KillSwitch result = client.masfeat().triggerKillSwitch("sys-789", request); + + assertThat(result.getStatus()).isEqualTo(KillSwitchStatus.TRIGGERED); + assertThat(result.getTriggeredReason()).isEqualTo("Manual trigger"); + + verify(postRequestedFor(urlEqualTo("/api/v1/masfeat/killswitch/sys-789/trigger"))); + } + + @Test + @DisplayName("Should restore kill switch") + void testRestoreKillSwitch() { + String responseJson = "{" + + "\"kill_switch\": {" + + "\"id\": \"ks-123\"," + + "\"org_id\": \"org-456\"," + + "\"system_id\": \"sys-789\"," + + "\"status\": \"enabled\"," + + "\"auto_trigger_enabled\": true," + + "\"created_at\": \"2026-01-23T12:00:00Z\"," + + "\"updated_at\": \"2026-01-23T12:00:00Z\"" + + "}," + + "\"message\": \"Kill switch restored\"" + + "}"; + stubFor(post(urlEqualTo("/api/v1/masfeat/killswitch/sys-789/restore")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(responseJson))); + + RestoreKillSwitchRequest request = RestoreKillSwitchRequest.builder().build(); + KillSwitch result = client.masfeat().restoreKillSwitch("sys-789", request); + + assertThat(result.getStatus()).isEqualTo(KillSwitchStatus.ENABLED); + + verify(postRequestedFor(urlEqualTo("/api/v1/masfeat/killswitch/sys-789/restore"))); + } + + @Test + @DisplayName("Should get kill switch history") + void testGetKillSwitchHistory() { + String responseJson = "{" + + "\"history\": [" + + "{\"id\": \"event-1\", \"kill_switch_id\": \"ks-123\", \"action\": \"enabled\", \"performed_by\": \"admin\", \"performed_at\": \"2026-01-23T12:00:00Z\"}," + + "{\"id\": \"event-2\", \"kill_switch_id\": \"ks-123\", \"action\": \"triggered\", \"reason\": \"Bias exceeded\", \"performed_by\": \"system\", \"performed_at\": \"2026-01-23T13:00:00Z\"}" + + "]," + + "\"count\": 2" + + "}"; + stubFor(get(urlEqualTo("/api/v1/masfeat/killswitch/sys-789/history?limit=10")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(responseJson))); + + List result = client.masfeat().getKillSwitchHistory("sys-789", 10); + + assertThat(result).hasSize(2); + assertThat(result.get(0).getEventType()).isEqualTo("enabled"); + assertThat(result.get(1).getEventType()).isEqualTo("triggered"); + + verify(getRequestedFor(urlEqualTo("/api/v1/masfeat/killswitch/sys-789/history?limit=10"))); + } + } +} diff --git a/src/test/java/com/getaxonflow/sdk/masfeat/MASFEATTypesTest.java b/src/test/java/com/getaxonflow/sdk/masfeat/MASFEATTypesTest.java new file mode 100644 index 0000000..cb3cb03 --- /dev/null +++ b/src/test/java/com/getaxonflow/sdk/masfeat/MASFEATTypesTest.java @@ -0,0 +1,503 @@ +/* + * Copyright 2026 AxonFlow + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + */ +package com.getaxonflow.sdk.masfeat; + +import com.getaxonflow.sdk.masfeat.MASFEATTypes.*; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.*; + +/** + * Tests for MAS FEAT compliance types. + */ +@DisplayName("MAS FEAT Types Tests") +class MASFEATTypesTest { + + // ========================================================================= + // Enum Tests + // ========================================================================= + + @Nested + @DisplayName("MaterialityClassification Enum Tests") + class MaterialityClassificationTests { + + @Test + @DisplayName("Should return correct values for all classifications") + void testEnumValues() { + assertThat(MaterialityClassification.HIGH.getValue()).isEqualTo("high"); + assertThat(MaterialityClassification.MEDIUM.getValue()).isEqualTo("medium"); + assertThat(MaterialityClassification.LOW.getValue()).isEqualTo("low"); + } + + @Test + @DisplayName("Should convert from string value") + void testFromValue() { + assertThat(MaterialityClassification.fromValue("high")).isEqualTo(MaterialityClassification.HIGH); + assertThat(MaterialityClassification.fromValue("medium")).isEqualTo(MaterialityClassification.MEDIUM); + assertThat(MaterialityClassification.fromValue("low")).isEqualTo(MaterialityClassification.LOW); + } + + @Test + @DisplayName("Should throw for unknown value") + void testFromValueUnknown() { + assertThatThrownBy(() -> MaterialityClassification.fromValue("invalid")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Unknown materiality"); + } + } + + @Nested + @DisplayName("SystemStatus Enum Tests") + class SystemStatusTests { + + @Test + @DisplayName("Should return correct values for all statuses") + void testEnumValues() { + assertThat(SystemStatus.DRAFT.getValue()).isEqualTo("draft"); + assertThat(SystemStatus.ACTIVE.getValue()).isEqualTo("active"); + assertThat(SystemStatus.SUSPENDED.getValue()).isEqualTo("suspended"); + assertThat(SystemStatus.RETIRED.getValue()).isEqualTo("retired"); + } + + @Test + @DisplayName("Should convert from string value") + void testFromValue() { + assertThat(SystemStatus.fromValue("draft")).isEqualTo(SystemStatus.DRAFT); + assertThat(SystemStatus.fromValue("active")).isEqualTo(SystemStatus.ACTIVE); + assertThat(SystemStatus.fromValue("suspended")).isEqualTo(SystemStatus.SUSPENDED); + assertThat(SystemStatus.fromValue("retired")).isEqualTo(SystemStatus.RETIRED); + } + + @Test + @DisplayName("Should throw for unknown value") + void testFromValueUnknown() { + assertThatThrownBy(() -> SystemStatus.fromValue("invalid")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Unknown status"); + } + } + + @Nested + @DisplayName("FEATAssessmentStatus Enum Tests") + class FEATAssessmentStatusTests { + + @Test + @DisplayName("Should return correct values for all statuses") + void testEnumValues() { + assertThat(FEATAssessmentStatus.PENDING.getValue()).isEqualTo("pending"); + assertThat(FEATAssessmentStatus.IN_PROGRESS.getValue()).isEqualTo("in_progress"); + assertThat(FEATAssessmentStatus.COMPLETED.getValue()).isEqualTo("completed"); + assertThat(FEATAssessmentStatus.APPROVED.getValue()).isEqualTo("approved"); + assertThat(FEATAssessmentStatus.REJECTED.getValue()).isEqualTo("rejected"); + } + + @Test + @DisplayName("Should convert from string value") + void testFromValue() { + assertThat(FEATAssessmentStatus.fromValue("pending")).isEqualTo(FEATAssessmentStatus.PENDING); + assertThat(FEATAssessmentStatus.fromValue("in_progress")).isEqualTo(FEATAssessmentStatus.IN_PROGRESS); + assertThat(FEATAssessmentStatus.fromValue("completed")).isEqualTo(FEATAssessmentStatus.COMPLETED); + assertThat(FEATAssessmentStatus.fromValue("approved")).isEqualTo(FEATAssessmentStatus.APPROVED); + assertThat(FEATAssessmentStatus.fromValue("rejected")).isEqualTo(FEATAssessmentStatus.REJECTED); + } + + @Test + @DisplayName("Should throw for unknown value") + void testFromValueUnknown() { + assertThatThrownBy(() -> FEATAssessmentStatus.fromValue("invalid")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Unknown assessment status"); + } + } + + @Nested + @DisplayName("KillSwitchStatus Enum Tests") + class KillSwitchStatusTests { + + @Test + @DisplayName("Should return correct values for all statuses") + void testEnumValues() { + assertThat(KillSwitchStatus.ENABLED.getValue()).isEqualTo("enabled"); + assertThat(KillSwitchStatus.DISABLED.getValue()).isEqualTo("disabled"); + assertThat(KillSwitchStatus.TRIGGERED.getValue()).isEqualTo("triggered"); + } + + @Test + @DisplayName("Should convert from string value") + void testFromValue() { + assertThat(KillSwitchStatus.fromValue("enabled")).isEqualTo(KillSwitchStatus.ENABLED); + assertThat(KillSwitchStatus.fromValue("disabled")).isEqualTo(KillSwitchStatus.DISABLED); + assertThat(KillSwitchStatus.fromValue("triggered")).isEqualTo(KillSwitchStatus.TRIGGERED); + } + + @Test + @DisplayName("Should throw for unknown value") + void testFromValueUnknown() { + assertThatThrownBy(() -> KillSwitchStatus.fromValue("invalid")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Unknown kill switch status"); + } + } + + @Nested + @DisplayName("AISystemUseCase Enum Tests") + class AISystemUseCaseTests { + + @Test + @DisplayName("Should return correct values for all use cases") + void testEnumValues() { + assertThat(AISystemUseCase.CREDIT_SCORING.getValue()).isEqualTo("credit_scoring"); + assertThat(AISystemUseCase.ROBO_ADVISORY.getValue()).isEqualTo("robo_advisory"); + assertThat(AISystemUseCase.INSURANCE_UNDERWRITING.getValue()).isEqualTo("insurance_underwriting"); + assertThat(AISystemUseCase.TRADING_ALGORITHM.getValue()).isEqualTo("trading_algorithm"); + assertThat(AISystemUseCase.AML_CFT.getValue()).isEqualTo("aml_cft"); + assertThat(AISystemUseCase.CUSTOMER_SERVICE.getValue()).isEqualTo("customer_service"); + assertThat(AISystemUseCase.FRAUD_DETECTION.getValue()).isEqualTo("fraud_detection"); + assertThat(AISystemUseCase.OTHER.getValue()).isEqualTo("other"); + } + + @Test + @DisplayName("Should convert from string value") + void testFromValue() { + assertThat(AISystemUseCase.fromValue("credit_scoring")).isEqualTo(AISystemUseCase.CREDIT_SCORING); + assertThat(AISystemUseCase.fromValue("robo_advisory")).isEqualTo(AISystemUseCase.ROBO_ADVISORY); + assertThat(AISystemUseCase.fromValue("fraud_detection")).isEqualTo(AISystemUseCase.FRAUD_DETECTION); + assertThat(AISystemUseCase.fromValue("other")).isEqualTo(AISystemUseCase.OTHER); + } + + @Test + @DisplayName("Should throw for unknown value") + void testFromValueUnknown() { + assertThatThrownBy(() -> AISystemUseCase.fromValue("invalid")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Unknown use case"); + } + } + + // ========================================================================= + // Request Builder Tests + // ========================================================================= + + @Nested + @DisplayName("RegisterSystemRequest Builder Tests") + class RegisterSystemRequestTests { + + @Test + @DisplayName("Should build with required fields") + void testBuilderWithRequiredFields() { + RegisterSystemRequest request = RegisterSystemRequest.builder() + .systemId("credit-model-v1") + .systemName("Credit Scoring Model") + .useCase(AISystemUseCase.CREDIT_SCORING) + .ownerTeam("data-science") + .customerImpact(3) + .modelComplexity(2) + .humanReliance(1) + .build(); + + assertThat(request.getSystemId()).isEqualTo("credit-model-v1"); + assertThat(request.getSystemName()).isEqualTo("Credit Scoring Model"); + assertThat(request.getUseCase()).isEqualTo(AISystemUseCase.CREDIT_SCORING); + assertThat(request.getOwnerTeam()).isEqualTo("data-science"); + assertThat(request.getCustomerImpact()).isEqualTo(3); + assertThat(request.getModelComplexity()).isEqualTo(2); + assertThat(request.getHumanReliance()).isEqualTo(1); + } + + @Test + @DisplayName("Should build with optional fields") + void testBuilderWithOptionalFields() { + Map metadata = new HashMap<>(); + metadata.put("version", "1.0"); + + RegisterSystemRequest request = RegisterSystemRequest.builder() + .systemId("credit-model-v1") + .systemName("Credit Scoring Model") + .useCase(AISystemUseCase.CREDIT_SCORING) + .ownerTeam("data-science") + .customerImpact(3) + .modelComplexity(2) + .humanReliance(1) + .description("AI model for credit scoring") + .technicalOwner("tech@example.com") + .businessOwner("business@example.com") + .metadata(metadata) + .build(); + + assertThat(request.getDescription()).isEqualTo("AI model for credit scoring"); + assertThat(request.getTechnicalOwner()).isEqualTo("tech@example.com"); + assertThat(request.getBusinessOwner()).isEqualTo("business@example.com"); + assertThat(request.getMetadata()).containsEntry("version", "1.0"); + } + } + + @Nested + @DisplayName("CreateAssessmentRequest Builder Tests") + class CreateAssessmentRequestTests { + + @Test + @DisplayName("Should build with required fields") + void testBuilderWithRequiredFields() { + CreateAssessmentRequest request = CreateAssessmentRequest.builder() + .systemId("credit-model-v1") + .assessmentType("annual") + .build(); + + assertThat(request.getSystemId()).isEqualTo("credit-model-v1"); + assertThat(request.getAssessmentType()).isEqualTo("annual"); + } + + @Test + @DisplayName("Should build with optional fields") + void testBuilderWithOptionalFields() { + CreateAssessmentRequest request = CreateAssessmentRequest.builder() + .systemId("credit-model-v1") + .assessmentType("annual") + .assessors(List.of("assessor1", "assessor2")) + .build(); + + assertThat(request.getAssessors()).containsExactly("assessor1", "assessor2"); + } + } + + @Nested + @DisplayName("ConfigureKillSwitchRequest Builder Tests") + class ConfigureKillSwitchRequestTests { + + @Test + @DisplayName("Should build with all thresholds") + void testBuilderWithAllThresholds() { + ConfigureKillSwitchRequest request = ConfigureKillSwitchRequest.builder() + .accuracyThreshold(0.95) + .biasThreshold(0.10) + .errorRateThreshold(0.05) + .autoTriggerEnabled(true) + .build(); + + assertThat(request.getAccuracyThreshold()).isEqualTo(0.95); + assertThat(request.getBiasThreshold()).isEqualTo(0.10); + assertThat(request.getErrorRateThreshold()).isEqualTo(0.05); + assertThat(request.getAutoTriggerEnabled()).isTrue(); + } + + @Test + @DisplayName("Should have null autoTriggerEnabled when not set") + void testAutoTriggerDefault() { + ConfigureKillSwitchRequest request = ConfigureKillSwitchRequest.builder() + .accuracyThreshold(0.95) + .build(); + + assertThat(request.getAutoTriggerEnabled()).isNull(); + } + } + + // ========================================================================= + // Response Type Tests (using setters) + // ========================================================================= + + @Nested + @DisplayName("AISystemRegistry Tests") + class AISystemRegistryTests { + + @Test + @DisplayName("Should set and get all fields") + void testSettersAndGetters() { + Instant now = Instant.now(); + Map metadata = Map.of("version", "1.0"); + + AISystemRegistry registry = new AISystemRegistry(); + registry.setId("sys-123"); + registry.setOrgId("org-456"); + registry.setSystemId("credit-model-v1"); + registry.setSystemName("Credit Scoring Model"); + registry.setDescription("AI model for credit scoring"); + registry.setUseCase(AISystemUseCase.CREDIT_SCORING); + registry.setOwnerTeam("data-science"); + registry.setTechnicalOwner("tech@example.com"); + registry.setBusinessOwner("business@example.com"); + registry.setCustomerImpact(3); + registry.setModelComplexity(2); + registry.setHumanReliance(1); + registry.setMateriality(MaterialityClassification.HIGH); + registry.setStatus(SystemStatus.ACTIVE); + registry.setMetadata(metadata); + registry.setCreatedAt(now); + registry.setUpdatedAt(now); + registry.setCreatedBy("admin"); + + assertThat(registry.getId()).isEqualTo("sys-123"); + assertThat(registry.getOrgId()).isEqualTo("org-456"); + assertThat(registry.getSystemId()).isEqualTo("credit-model-v1"); + assertThat(registry.getSystemName()).isEqualTo("Credit Scoring Model"); + assertThat(registry.getDescription()).isEqualTo("AI model for credit scoring"); + assertThat(registry.getUseCase()).isEqualTo(AISystemUseCase.CREDIT_SCORING); + assertThat(registry.getOwnerTeam()).isEqualTo("data-science"); + assertThat(registry.getTechnicalOwner()).isEqualTo("tech@example.com"); + assertThat(registry.getBusinessOwner()).isEqualTo("business@example.com"); + assertThat(registry.getCustomerImpact()).isEqualTo(3); + assertThat(registry.getModelComplexity()).isEqualTo(2); + assertThat(registry.getHumanReliance()).isEqualTo(1); + assertThat(registry.getMateriality()).isEqualTo(MaterialityClassification.HIGH); + assertThat(registry.getStatus()).isEqualTo(SystemStatus.ACTIVE); + assertThat(registry.getMetadata()).containsEntry("version", "1.0"); + assertThat(registry.getCreatedAt()).isEqualTo(now); + assertThat(registry.getUpdatedAt()).isEqualTo(now); + assertThat(registry.getCreatedBy()).isEqualTo("admin"); + } + } + + @Nested + @DisplayName("RegistrySummary Tests") + class RegistrySummaryTests { + + @Test + @DisplayName("Should set and get all fields") + void testSettersAndGetters() { + Map byUseCase = Map.of( + "credit_scoring", 4, + "fraud_detection", 6 + ); + Map byStatus = Map.of( + "active", 8, + "draft", 2 + ); + + RegistrySummary summary = new RegistrySummary(); + summary.setTotalSystems(10); + summary.setActiveSystems(8); + summary.setHighMaterialityCount(2); + summary.setMediumMaterialityCount(5); + summary.setLowMaterialityCount(3); + summary.setByUseCase(byUseCase); + summary.setByStatus(byStatus); + + assertThat(summary.getTotalSystems()).isEqualTo(10); + assertThat(summary.getActiveSystems()).isEqualTo(8); + assertThat(summary.getHighMaterialityCount()).isEqualTo(2); + assertThat(summary.getMediumMaterialityCount()).isEqualTo(5); + assertThat(summary.getLowMaterialityCount()).isEqualTo(3); + assertThat(summary.getByUseCase()).containsEntry("credit_scoring", 4); + assertThat(summary.getByStatus()).containsEntry("active", 8); + } + } + + @Nested + @DisplayName("FEATAssessment Tests") + class FEATAssessmentTests { + + @Test + @DisplayName("Should set and get all fields") + void testSettersAndGetters() { + Instant now = Instant.now(); + + FEATAssessment assessment = new FEATAssessment(); + assessment.setId("assess-123"); + assessment.setOrgId("org-456"); + assessment.setSystemId("sys-789"); + assessment.setAssessmentType("annual"); + assessment.setStatus(FEATAssessmentStatus.COMPLETED); + assessment.setAssessmentDate(now); + assessment.setValidUntil(now.plusSeconds(86400 * 365)); + assessment.setFairnessScore(85); + assessment.setEthicsScore(90); + assessment.setAccountabilityScore(88); + assessment.setTransparencyScore(92); + assessment.setOverallScore(89); + Finding finding = Finding.builder() + .id("f-1") + .pillar(FEATPillar.FAIRNESS) + .severity(FindingSeverity.MINOR) + .category("test-category") + .description("Finding 1") + .status(FindingStatus.OPEN) + .build(); + assessment.setFindings(List.of(finding)); + assessment.setRecommendations(List.of("Recommendation 1")); + assessment.setAssessors(List.of("assessor1")); + assessment.setApprovedBy("approver@example.com"); + assessment.setApprovedAt(now); + assessment.setCreatedAt(now); + assessment.setUpdatedAt(now); + assessment.setCreatedBy("admin"); + + assertThat(assessment.getId()).isEqualTo("assess-123"); + assertThat(assessment.getOrgId()).isEqualTo("org-456"); + assertThat(assessment.getSystemId()).isEqualTo("sys-789"); + assertThat(assessment.getAssessmentType()).isEqualTo("annual"); + assertThat(assessment.getStatus()).isEqualTo(FEATAssessmentStatus.COMPLETED); + assertThat(assessment.getFairnessScore()).isEqualTo(85); + assertThat(assessment.getEthicsScore()).isEqualTo(90); + assertThat(assessment.getAccountabilityScore()).isEqualTo(88); + assertThat(assessment.getTransparencyScore()).isEqualTo(92); + assertThat(assessment.getOverallScore()).isEqualTo(89); + assertThat(assessment.getFindings()).hasSize(1); + assertThat(assessment.getFindings().get(0).getDescription()).isEqualTo("Finding 1"); + assertThat(assessment.getRecommendations()).containsExactly("Recommendation 1"); + assertThat(assessment.getAssessors()).containsExactly("assessor1"); + assertThat(assessment.getApprovedBy()).isEqualTo("approver@example.com"); + } + } + + @Nested + @DisplayName("KillSwitch Tests") + class KillSwitchTests { + + @Test + @DisplayName("Should set and get all fields") + void testSettersAndGetters() { + Instant now = Instant.now(); + + KillSwitch ks = new KillSwitch(); + ks.setId("ks-123"); + ks.setOrgId("org-456"); + ks.setSystemId("sys-789"); + ks.setStatus(KillSwitchStatus.ENABLED); + ks.setAccuracyThreshold(0.95); + ks.setBiasThreshold(0.10); + ks.setErrorRateThreshold(0.05); + ks.setAutoTriggerEnabled(true); + ks.setCreatedAt(now); + ks.setUpdatedAt(now); + + assertThat(ks.getId()).isEqualTo("ks-123"); + assertThat(ks.getOrgId()).isEqualTo("org-456"); + assertThat(ks.getSystemId()).isEqualTo("sys-789"); + assertThat(ks.getStatus()).isEqualTo(KillSwitchStatus.ENABLED); + assertThat(ks.getAccuracyThreshold()).isEqualTo(0.95); + assertThat(ks.getBiasThreshold()).isEqualTo(0.10); + assertThat(ks.getErrorRateThreshold()).isEqualTo(0.05); + assertThat(ks.isAutoTriggerEnabled()).isTrue(); + } + + @Test + @DisplayName("Should handle triggered state") + void testTriggeredState() { + Instant now = Instant.now(); + + KillSwitch ks = new KillSwitch(); + ks.setId("ks-123"); + ks.setOrgId("org-456"); + ks.setSystemId("sys-789"); + ks.setStatus(KillSwitchStatus.TRIGGERED); + ks.setTriggeredAt(now); + ks.setTriggeredBy("admin"); + ks.setTriggeredReason("Bias threshold exceeded"); + + assertThat(ks.getStatus()).isEqualTo(KillSwitchStatus.TRIGGERED); + assertThat(ks.getTriggeredAt()).isEqualTo(now); + assertThat(ks.getTriggeredBy()).isEqualTo("admin"); + assertThat(ks.getTriggeredReason()).isEqualTo("Bias threshold exceeded"); + } + } +}