diff --git a/CHANGELOG.md b/CHANGELOG.md index fd20eb8..480602c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- masfeat real wire mapping (#3254 pin-advance batch). `RegistrySummary` + gains the real `org_id` (`getOrgId()`), `assessments_due` + (`getAssessmentsDue()`) and `kill_switches_triggered` + (`getKillSwitchesTriggered()`) fields, and its parser now reads the real + `high_materiality`/`medium_materiality`/`low_materiality` keys first + (previously `medium`/`low` were read ONLY under the never-served `_count` + spelling and were always 0 against a real server; the legacy spelling is + kept as a fallback). `AISystemRegistry` gains `owner_email` + (`getOwnerEmail()`, the real wire key; `getBusinessOwner()` remains as a + populated compatibility alias) and its parser prefers the real + `materiality_classification` key. `KillSwitch`'s parser now prefers the + real `trigger_reason` key (the server has never sent `triggered_reason`). + All three models now carry `@JsonProperty` tags with the REAL wire names + so the Jackson surface tells the truth and the wire-shape binding gate + can bind them; the hand-written parsers remain the IO path. +- Wire-shape Gate 5 extended to the masfeat models `RegistrySummary`, + `KillSwitch` and `AISystemRegistry` (nested-class binding support + + per-type source registration for the freshness guard). + `OJKAuditExportResponse` is not modeled by this SDK - nothing to bind. + - Real wire fields `policy_decision` (`getPolicyDecision()`), `policy_details` (`getPolicyDetails()`), `response_time_ms` (`getResponseTimeMs()`) on the audit read model (`AuditLogEntry`), and `action` (`Builder.action(String)`) @@ -30,6 +50,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Deprecated +- `RegistrySummary.getByUseCase()`/`getByStatus()` (and setters) and + `AISystemRegistry.getTechnicalOwner()`/`setTechnicalOwner()` - never + served on the 9.x line (#3254 pin-advance batch). Removal rides the next + major. + - `query_summary`/`success`/`blocked`/`risk_score`/`latency_ms`/ `policy_violations`/`metadata` (read model) and `request_type` (search request) - never served/read on the 9.x line (#3254). Removal rides the diff --git a/runtime-e2e/masfeat_registry_summary/MasfeatRegistrySummaryTest.java b/runtime-e2e/masfeat_registry_summary/MasfeatRegistrySummaryTest.java new file mode 100644 index 0000000..249a5d7 --- /dev/null +++ b/runtime-e2e/masfeat_registry_summary/MasfeatRegistrySummaryTest.java @@ -0,0 +1,112 @@ +/* + * runtime-e2e/masfeat_registry_summary/MasfeatRegistrySummaryTest.java + * + * Real-stack leg for the #3254 pin-advance batch (masfeat models). + * + * MAS FEAT is an Enterprise module: on a community build the orchestrator + * registers NO masfeat routes (platform/orchestrator/masfeat/ + * masfeat_community.go RegisterRoutes is a no-op), so against a community + * stack the correct observable behavior of the SDK is a clean HTTP-level + * refusal (404 route-not-found surfaced as an AxonFlowException), NOT a + * parse error and NOT a fabricated summary object. + * + * This test therefore asserts one of two legitimate outcomes, and prints + * which one it exercised: + * + * ENTERPRISE leg: getRegistrySummary() succeeds; the #3254 real fields + * (org_id-derived getOrgId, assessments_due, kill_switches_triggered, + * medium/low materiality counters) are readable and the deprecated + * by_use_case / by_status fiction maps are null. + * + * COMMUNITY leg: the call fails with an HTTP-level error (route absent / + * gated) - and specifically NOT a JSON parse failure, which would mean + * the SDK mis-handled the gate. + * + * Env: + * AXONFLOW_ENDPOINT agent URL (default http://127.0.0.1:38080) + * AXONFLOW_CLIENT_ID client identity (default demo-client) + * AXONFLOW_CLIENT_SECRET client secret (default demo-secret) + * + * Run (from the SDK root): + * + * mvn install -DskipTests + * mvn -q dependency:build-classpath -Dmdep.outputFile=/tmp/cp.txt + * SDK_JAR=$(ls target/axonflow-sdk-*.jar | grep -v sources | grep -v javadoc | head -1) + * java -cp "$SDK_JAR:$(cat /tmp/cp.txt)" \ + * runtime-e2e/masfeat_registry_summary/MasfeatRegistrySummaryTest.java + */ +import com.getaxonflow.sdk.AxonFlow; +import com.getaxonflow.sdk.AxonFlowConfig; +import com.getaxonflow.sdk.exceptions.AxonFlowException; +import com.getaxonflow.sdk.masfeat.MASFEATTypes.RegistrySummary; + +public class MasfeatRegistrySummaryTest { + + static void fail(String msg) { + System.err.println("FAIL: " + msg); + System.exit(1); + } + + static String env(String name, String dflt) { + String v = System.getenv(name); + return (v == null || v.isEmpty()) ? dflt : v; + } + + @SuppressWarnings("deprecation") + public static void main(String[] args) { + String endpoint = env("AXONFLOW_ENDPOINT", "http://127.0.0.1:38080"); + + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(endpoint) + .clientId(env("AXONFLOW_CLIENT_ID", "demo-client")) + .clientSecret(env("AXONFLOW_CLIENT_SECRET", "demo-secret")) + .build()); + + RegistrySummary summary; + try { + summary = client.masfeat().getRegistrySummary(); + } catch (AxonFlowException e) { + String msg = e.getMessage() == null ? "" : e.getMessage(); + if (msg.contains("Failed to parse")) { + fail("community gate surfaced as a PARSE error - the SDK mishandled the refusal: " + msg); + } + System.out.println( + "PASS [community-gate] masfeat route refused cleanly by a community stack " + + "(enterprise-only module, no routes registered): " + + e.getClass().getSimpleName() + + ": " + + msg); + System.out.println( + "NOTE: enterprise leg NOT exercised on this stack - real-field assertions " + + "rest on the source-derived WireMock suite " + + "(src/test/java/com/getaxonflow/sdk/masfeat/MASFEATRealWireTest.java)."); + return; + } + + // Enterprise leg: the real #3254 fields must be readable. + System.out.println( + "PASS [enterprise-live] registry summary: orgId=" + + summary.getOrgId() + + " total=" + + summary.getTotalSystems() + + " active=" + + summary.getActiveSystems() + + " high=" + + summary.getHighMaterialityCount() + + " medium=" + + summary.getMediumMaterialityCount() + + " low=" + + summary.getLowMaterialityCount() + + " assessmentsDue=" + + summary.getAssessmentsDue() + + " killSwitchesTriggered=" + + summary.getKillSwitchesTriggered()); + if (summary.getByUseCase() != null || summary.getByStatus() != null) { + fail("deprecated by_use_case/by_status came back non-null - the server never serves them; " + + "a non-null value means the model regressed into fiction"); + } + System.out.println("PASS [deprecated-defaults] by_use_case/by_status null as expected"); + } +} diff --git a/runtime-e2e/masfeat_registry_summary/README.md b/runtime-e2e/masfeat_registry_summary/README.md new file mode 100644 index 0000000..eca9fec --- /dev/null +++ b/runtime-e2e/masfeat_registry_summary/README.md @@ -0,0 +1,35 @@ +# masfeat_registry_summary (masfeat real-wire fields, #3254 pin-advance batch) + +Real-stack leg for the getaxonflow/axonflow-enterprise#3254 pin-advance +batch: the masfeat models now carry the real wire mapping (see +`MASFEATTypes` and the `MASFEATNamespace` parsers). + +MAS FEAT is an Enterprise module. On a community build the orchestrator +registers no masfeat routes (`masfeat_community.go` `RegisterRoutes` is a +no-op), so this test asserts one of two legitimate outcomes through the +SDK's real public surface (`client.masfeat().getRegistrySummary()`) +against a real running agent, no mocks: + +- **Enterprise leg:** the summary parses; the #3254 real fields + (`org_id`, `assessments_due`, `kill_switches_triggered`, the + suffix-less materiality counters) are readable; the deprecated + `by_use_case`/`by_status` fiction maps are null. +- **Community leg:** the call is refused at the HTTP level (route + absent) and the SDK surfaces a clean `AxonFlowException` - NOT a + parse failure. The test prints a NOTE that the enterprise leg was not + exercised; real-field assertions then rest on the source-derived + WireMock suite (`MASFEATRealWireTest`). + +## Run + +```bash +export AXONFLOW_ENDPOINT=http://127.0.0.1:38080 # default +export AXONFLOW_CLIENT_ID=demo-client # default +export AXONFLOW_CLIENT_SECRET=demo-secret # default + +mvn install -DskipTests +mvn -q dependency:build-classpath -Dmdep.outputFile=/tmp/cp.txt +SDK_JAR=$(ls target/axonflow-sdk-*.jar | grep -v sources | grep -v javadoc | head -1) +java -cp "$SDK_JAR:$(cat /tmp/cp.txt)" \ + runtime-e2e/masfeat_registry_summary/MasfeatRegistrySummaryTest.java +``` diff --git a/scripts/wire_shape/AuditWireKeysProbe.java b/scripts/wire_shape/AuditWireKeysProbe.java index a7e3bcb..08514ce 100644 --- a/scripts/wire_shape/AuditWireKeysProbe.java +++ b/scripts/wire_shape/AuditWireKeysProbe.java @@ -36,7 +36,11 @@ * fully-qualified class name passed as an argument it asks Jackson - configured EXACTLY as * production configures it, by reflecting the private {@code AxonFlow.createObjectMapper()} * factory - for the wire property names, as the union of the serialization and deserialization - * bean descriptions, and prints one JSON object mapping simple class name to sorted wire keys. + * bean descriptions, and prints one JSON object of the shape {@code {SimpleName: {"keys": + * [sorted wire keys], "deprecated": [subset whose backing member - field, getter, setter, or + * creator parameter - carries {@code @Deprecated}]}}}. The {@code deprecated} set lets the + * caller enforce the deprecation tie: an allowlisted fiction key must be visibly deprecated in + * the model, not silently tolerated. * *
Why introspection instead of source-regex discovery: a regex over the source cannot resolve a
* constant-valued annotation ({@code @JsonProperty(SOME_CONSTANT)}) and cannot see Jackson's
@@ -90,21 +94,31 @@ public static void main(String[] args) {
}
try {
ObjectMapper mapper = productionConfiguredMapper();
- TreeMap Wire mapping note (#3254): the {@code @JsonProperty} tags carry the REAL wire names served
+ * by the orchestrator's {@code masfeat.AISystemRegistry} struct. The SDK populates this type via
+ * a hand-written parser ({@code AxonFlow.MASFEATNamespace}); the tags exist so the Jackson
+ * surface is truthful and the wire-shape binding gate can bind this model to the spec. The
+ * {@code customerImpact}/{@code modelComplexity}/{@code humanReliance} JAVA names are historic;
+ * their wire names are {@code risk_rating_impact}/{@code risk_rating_complexity}/{@code
+ * risk_rating_reliance} and the data is real. {@code businessOwner} is a compatibility alias
+ * populated from the wire's {@code owner_email}; prefer {@link #getOwnerEmail()}.
+ */
public static class AISystemRegistry {
+ @JsonProperty("id")
private String id;
+
+ @JsonProperty("org_id")
private String orgId;
+
+ @JsonProperty("system_id")
private String systemId;
+
+ @JsonProperty("system_name")
private String systemName;
+
+ @JsonProperty("use_case")
private AISystemUseCase useCase;
+
+ @JsonProperty("owner_team")
private String ownerTeam;
+
+ @JsonProperty("risk_rating_impact")
private int customerImpact;
+
+ @JsonProperty("risk_rating_complexity")
private int modelComplexity;
+
+ @JsonProperty("risk_rating_reliance")
private int humanReliance;
- @com.fasterxml.jackson.annotation.JsonProperty("materiality_classification")
+ @JsonProperty("materiality_classification")
private MaterialityClassification materialityClassification;
+ @JsonProperty("status")
private SystemStatus status;
+
+ @JsonProperty("created_at")
private Instant createdAt;
+
+ @JsonProperty("updated_at")
private Instant updatedAt;
+
+ @JsonProperty("description")
private String description;
+
+ @JsonProperty("technical_owner")
private String technicalOwner;
+
private String businessOwner;
+
+ @JsonProperty("owner_email")
+ private String ownerEmail;
+
+ @JsonProperty("metadata")
private Map Wire mapping note (#3254): the server's {@code masfeat.RegistrySummary} serves {@code
+ * org_id}, {@code total_systems}, {@code active_systems}, {@code high_materiality}, {@code
+ * medium_materiality}, {@code low_materiality}, {@code assessments_due} and {@code
+ * kill_switches_triggered}. The {@code *MaterialityCount} JAVA names are historic; their wire
+ * names have no {@code _count} suffix and the data is real. {@code byUseCase}/{@code byStatus}
+ * are deprecated fiction - see their getters.
+ */
public static class RegistrySummary {
+ @JsonProperty("org_id")
+ private String orgId;
+
+ @JsonProperty("total_systems")
private int totalSystems;
+
+ @JsonProperty("active_systems")
private int activeSystems;
+
+ @JsonProperty("high_materiality")
private int highMaterialityCount;
+
+ @JsonProperty("medium_materiality")
private int mediumMaterialityCount;
+
+ @JsonProperty("low_materiality")
private int lowMaterialityCount;
+
+ @JsonProperty("assessments_due")
+ private int assessmentsDue;
+
+ @JsonProperty("kill_switches_triggered")
+ private int killSwitchesTriggered;
+
+ @JsonProperty("by_use_case")
private Map Fixture provenance: every payload in this class is SOURCE-DERIVED from the orchestrator's
+ * masfeat structs ({@code platform/orchestrator/masfeat/types.go} at community tag v9.13.0,
+ * df027c788) and the pinned {@code masfeat-api.yaml} schemas - these are NOT live captures (the
+ * masfeat module is an Enterprise feature; the community stack registers no masfeat routes, see
+ * {@code masfeat_community.go}). Payloads marked "both spellings" are hand-constructed
+ * discriminators and say so.
+ *
+ * These tests exercise the SDK's REAL parse path - the hand-written parsers in {@code
+ * AxonFlow.MASFEATNamespace} reached through the public client methods over WireMock - not the
+ * post-parse object shape (a #3254-class fiction lives in what the parser READS, so only
+ * driving the parser can catch it).
+ *
+ * Every assertion on a #3254 fix is mutation-proof by construction: the asserted value differs
+ * from what the PRE-FIX parser would have produced (0 / null / the legacy key's value).
+ */
+@WireMockTest
+@DisplayName("MAS FEAT real-wire parsing (#3254)")
+class MASFEATRealWireTest {
+
+ private AxonFlow client;
+
+ @BeforeEach
+ void setUp(WireMockRuntimeInfo wmRuntimeInfo) {
+ client =
+ AxonFlow.create(
+ AxonFlow.builder()
+ .endpoint(wmRuntimeInfo.getHttpBaseUrl())
+ .clientId("test-client")
+ .clientSecret("test-secret")
+ .build());
+ }
+
+ @Test
+ @DisplayName("RegistrySummary - server-shaped payload populates every real field")
+ @SuppressWarnings("deprecation")
+ void registrySummaryServerShape() {
+ // Source-derived from masfeat.RegistrySummary (types.go:431-440): the
+ // real keys carry NO _count suffix. Distinct values everywhere so a
+ // wrong-key read cannot accidentally pass. The pre-#3254 parser read
+ // medium/low ONLY under the _count fiction spelling (always 0 against
+ // this payload) and never read org_id / assessments_due /
+ // kill_switches_triggered at all.
+ String responseJson =
+ "{"
+ + "\"org_id\": \"org-mas-1\","
+ + "\"total_systems\": 11,"
+ + "\"active_systems\": 7,"
+ + "\"high_materiality\": 2,"
+ + "\"medium_materiality\": 5,"
+ + "\"low_materiality\": 4,"
+ + "\"assessments_due\": 3,"
+ + "\"kill_switches_triggered\": 1"
+ + "}";
+ 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.getOrgId()).isEqualTo("org-mas-1");
+ assertThat(result.getTotalSystems()).isEqualTo(11);
+ assertThat(result.getActiveSystems()).isEqualTo(7);
+ assertThat(result.getHighMaterialityCount()).isEqualTo(2);
+ assertThat(result.getMediumMaterialityCount()).isEqualTo(5);
+ assertThat(result.getLowMaterialityCount()).isEqualTo(4);
+ assertThat(result.getAssessmentsDue()).isEqualTo(3);
+ assertThat(result.getKillSwitchesTriggered()).isEqualTo(1);
+ // The server's RegistrySummary has no by_use_case / by_status - the
+ // deprecated fiction maps stay null.
+ assertThat(result.getByUseCase()).isNull();
+ assertThat(result.getByStatus()).isNull();
+ }
+
+ @Test
+ @DisplayName("RegistrySummary - real key wins over legacy _count spelling when both present")
+ void registrySummaryRealKeyWins() {
+ // Hand-constructed discriminator (both spellings, different values, not
+ // a capture): the real key must win even when it is 0. The pre-#3254
+ // parser preferred the _count spelling for high and read ONLY the
+ // _count spelling for medium/low.
+ String responseJson =
+ "{"
+ + "\"total_systems\": 1,"
+ + "\"high_materiality\": 0,"
+ + "\"high_materiality_count\": 9,"
+ + "\"medium_materiality\": 6,"
+ + "\"medium_materiality_count\": 9,"
+ + "\"low_materiality\": 5,"
+ + "\"low_materiality_count\": 9"
+ + "}";
+ 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.getHighMaterialityCount()).isEqualTo(0);
+ assertThat(result.getMediumMaterialityCount()).isEqualTo(6);
+ assertThat(result.getLowMaterialityCount()).isEqualTo(5);
+ }
+
+ @Test
+ @DisplayName("RegistrySummary - legacy _count-only payload still parses (fallback tolerance)")
+ void registrySummaryLegacyFallback() {
+ // Hand-constructed legacy shape (fiction spellings only, not a capture):
+ // the fallback keeps tolerating it.
+ String responseJson =
+ "{"
+ + "\"total_systems\": 4,"
+ + "\"high_materiality_count\": 1,"
+ + "\"medium_materiality_count\": 2,"
+ + "\"low_materiality_count\": 1"
+ + "}";
+ 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.getHighMaterialityCount()).isEqualTo(1);
+ assertThat(result.getMediumMaterialityCount()).isEqualTo(2);
+ assertThat(result.getLowMaterialityCount()).isEqualTo(1);
+ // Fields absent from a legacy payload default cleanly.
+ assertThat(result.getOrgId()).isNull();
+ assertThat(result.getAssessmentsDue()).isEqualTo(0);
+ assertThat(result.getKillSwitchesTriggered()).isEqualTo(0);
+ }
+
+ @Test
+ @DisplayName("KillSwitch - trigger_reason (real key) wins over triggered_reason")
+ void killSwitchTriggerReasonRealKeyWins() {
+ // Source-derived from masfeat.KillSwitch (types.go:283-303) plus a
+ // hand-injected legacy spelling as a discriminator: the server serves
+ // trigger_reason; triggered_reason has never been sent. The pre-#3254
+ // parser preferred the legacy spelling, so it would return
+ // "legacy-fiction" here.
+ String responseJson =
+ "{"
+ + "\"id\": \"ks-1\","
+ + "\"org_id\": \"org-mas-1\","
+ + "\"system_id\": \"credit-model-v1\","
+ + "\"status\": \"triggered\","
+ + "\"trigger_reason\": \"accuracy breach\","
+ + "\"triggered_reason\": \"legacy-fiction\","
+ + "\"auto_trigger_enabled\": true,"
+ + "\"accuracy_threshold\": 0.95,"
+ + "\"triggered_at\": \"2026-08-01T10:00:00Z\","
+ + "\"triggered_by\": \"ops@bank.sg\","
+ + "\"created_at\": \"2026-07-01T10:00:00Z\","
+ + "\"updated_at\": \"2026-08-01T10:00:00Z\""
+ + "}";
+ stubFor(
+ get(urlEqualTo("/api/v1/masfeat/killswitch/credit-model-v1"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(responseJson)));
+
+ KillSwitch result = client.masfeat().getKillSwitch("credit-model-v1");
+
+ assertThat(result.getTriggeredReason()).isEqualTo("accuracy breach");
+ assertThat(result.getStatus()).isEqualTo(KillSwitchStatus.TRIGGERED);
+ assertThat(result.isAutoTriggerEnabled()).isTrue();
+ assertThat(result.getAccuracyThreshold()).isEqualTo(0.95);
+ assertThat(result.getTriggeredBy()).isEqualTo("ops@bank.sg");
+ }
+
+ @Test
+ @DisplayName("AISystemRegistry - owner_email lands in ownerEmail AND the businessOwner alias")
+ void aiSystemOwnerEmail() {
+ // Source-derived from masfeat.AISystemRegistry (types.go:172-198): the
+ // wire serves owner_email + owner_team; technical_owner has never been
+ // sent. materiality_classification carries a both-spellings
+ // discriminator (hand-injected "materiality" decoy): the real key must
+ // win - the pre-#3254 parser read "materiality" first, so it would
+ // report LOW here.
+ String responseJson =
+ "{"
+ + "\"id\": \"sys-9\","
+ + "\"org_id\": \"org-mas-1\","
+ + "\"system_id\": \"fraud-model-v2\","
+ + "\"system_name\": \"Fraud Detection v2\","
+ + "\"use_case\": \"fraud_detection\","
+ + "\"status\": \"active\","
+ + "\"owner_team\": \"risk-analytics\","
+ + "\"owner_email\": \"owner@bank.sg\","
+ + "\"risk_rating_impact\": 4,"
+ + "\"risk_rating_complexity\": 3,"
+ + "\"risk_rating_reliance\": 5,"
+ + "\"materiality_classification\": \"high\","
+ + "\"materiality\": \"low\","
+ + "\"created_at\": \"2026-05-01T00:00:00Z\","
+ + "\"updated_at\": \"2026-06-01T00:00:00Z\","
+ + "\"created_by\": \"admin@bank.sg\""
+ + "}";
+ stubFor(
+ get(urlEqualTo("/api/v1/masfeat/registry/fraud-model-v2"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(responseJson)));
+
+ AISystemRegistry result = client.masfeat().getSystem("fraud-model-v2");
+
+ assertThat(result.getOwnerEmail()).isEqualTo("owner@bank.sg");
+ assertThat(result.getBusinessOwner()).isEqualTo("owner@bank.sg");
+ assertThat(result.getOwnerTeam()).isEqualTo("risk-analytics");
+ // technical_owner is never served: the deprecated accessor stays null.
+ @SuppressWarnings("deprecation")
+ String technicalOwner = result.getTechnicalOwner();
+ assertThat(technicalOwner).isNull();
+ // Real risk_rating_* keys land in the historic java names.
+ assertThat(result.getCustomerImpact()).isEqualTo(4);
+ assertThat(result.getModelComplexity()).isEqualTo(3);
+ assertThat(result.getHumanReliance()).isEqualTo(5);
+ // materiality_classification (real) wins over the "materiality" decoy.
+ assertThat(result.getMaterialityClassification()).isEqualTo(MaterialityClassification.HIGH);
+ }
+
+ @Test
+ @DisplayName("Minimal payloads - new fields absent, everything defaults, no throw")
+ void absenceTolerance() {
+ stubFor(
+ get(urlEqualTo("/api/v1/masfeat/registry/summary"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"total_systems\": 0}")));
+
+ RegistrySummary summary = client.masfeat().getRegistrySummary();
+ assertThat(summary.getOrgId()).isNull();
+ assertThat(summary.getAssessmentsDue()).isEqualTo(0);
+ assertThat(summary.getKillSwitchesTriggered()).isEqualTo(0);
+
+ stubFor(
+ get(urlEqualTo("/api/v1/masfeat/registry/minimal-sys"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"id\": \"sys-min\", \"system_id\": \"minimal-sys\"}")));
+
+ AISystemRegistry system = client.masfeat().getSystem("minimal-sys");
+ assertThat(system.getOwnerEmail()).isNull();
+ assertThat(system.getBusinessOwner()).isNull();
+ }
+}
diff --git a/tests/fixtures/audit-binding-allowlist.json b/tests/fixtures/audit-binding-allowlist.json
index 4a777bc..55ba4a0 100644
--- a/tests/fixtures/audit-binding-allowlist.json
+++ b/tests/fixtures/audit-binding-allowlist.json
@@ -1,5 +1,5 @@
{
- "_comment": "Curated allowlist for wire-shape Gate 5 (audit-surface binding). Every entry is an SDK @JsonProperty field with NO backing property in the pinned OpenAPI schema - i.e. named, tracked debt. Entries here MUST carry a note naming the tracking issue. The gate fails on any unlisted unbound field and on any stale entry, so this file can only ever shrink toward empty. See scripts/wire_shape/validate.py.",
+ "_comment": "Curated allowlist for wire-shape Gate 5 (audit-surface binding). Every entry is an SDK @JsonProperty field with NO backing property in the pinned OpenAPI schema - i.e. named, tracked debt. Entries here MUST carry a note naming the tracking issue. The gate fails on any unlisted unbound field and on any stale entry, so this file can only ever shrink toward empty. See scripts/wire_shape/validate.py. Deprecation tie: every entry must be backed by an @Deprecated member in the model, UNLESS its note contains the word 'alias' - which declares a parser-populated compatibility alias carrying real data.",
"AuditLogEntry": {
"query_summary": "Never served on the 9.x line; deprecated in-model, removal rides the next major (getaxonflow/axonflow-enterprise#3254). The wire carries query/query_hash instead.",
"success": "Never served on the 9.x line; deprecated in-model, removal rides the next major (getaxonflow/axonflow-enterprise#3254). policy_decision 'allowed' replaces success=true.",
@@ -11,5 +11,13 @@
},
"AuditSearchRequest": {
"request_type": "The 9.x server does not read this filter (silent no-op); deprecated in-model in favor of action, removal rides the next major (getaxonflow/axonflow-enterprise#3254). Still sent on the wire, harmless."
+ },
+ "RegistrySummary": {
+ "by_use_case": "Never populated on the 9.x line - the server's RegistrySummary has never carried by_use_case (getaxonflow/axonflow-enterprise#3254). Deprecated in-model, removal rides the next major.",
+ "by_status": "Never populated on the 9.x line - the server's RegistrySummary has never carried by_status (getaxonflow/axonflow-enterprise#3254). Deprecated in-model, removal rides the next major."
+ },
+ "AISystemRegistry": {
+ "technical_owner": "Never populated on the 9.x line - the server's AISystemRegistry has never carried technical_owner; the wire serves owner_email + owner_team (getaxonflow/axonflow-enterprise#3254). Deprecated in-model, removal rides the next major.",
+ "businessOwner": "Not a wire key: compatibility alias field populated by the hand-written parser from the wire's owner_email (getaxonflow/axonflow-enterprise#3254). Prefer getOwnerEmail(); alias retained for source compatibility."
}
}
diff --git a/tests/fixtures/wire-shape-baseline.json b/tests/fixtures/wire-shape-baseline.json
index 41a3076..8d716bb 100644
--- a/tests/fixtures/wire-shape-baseline.json
+++ b/tests/fixtures/wire-shape-baseline.json
@@ -176,6 +176,21 @@
"intra_file_duplicates": {},
"openapi_specs_sha": "df027c788b60c18d044278c45aa4bce3a1ac8717",
"per_type_drift": {
+ "AISystemRegistry": {
+ "sdk_only": [
+ "businessOwner",
+ "technical_owner"
+ ],
+ "spec_only": [
+ "data_sources",
+ "deployment_date",
+ "last_assessment_date",
+ "model_type",
+ "next_assessment_due",
+ "updated_by",
+ "version"
+ ]
+ },
"AuditLogEntry": {
"sdk_only": [
"blocked",
@@ -240,6 +255,21 @@
"metadata"
]
},
+ "ConfigureKillSwitchRequest": {
+ "sdk_only": [
+ "accuracyThreshold",
+ "autoTriggerEnabled",
+ "biasThreshold",
+ "errorRateThreshold"
+ ],
+ "spec_only": [
+ "accuracy_threshold",
+ "auto_trigger_enabled",
+ "bias_threshold",
+ "error_rate_threshold",
+ "trigger_conditions"
+ ]
+ },
"ConnectorInfo": {
"sdk_only": [
"config_schema",
@@ -249,6 +279,16 @@
"healthy"
]
},
+ "CreateAssessmentRequest": {
+ "sdk_only": [
+ "assessmentType",
+ "systemId"
+ ],
+ "spec_only": [
+ "assessment_type",
+ "system_id"
+ ]
+ },
"CreateStaticPolicyRequest": {
"sdk_only": [],
"spec_only": [
@@ -344,6 +384,65 @@
],
"spec_only": []
},
+ "FEATAssessment": {
+ "sdk_only": [
+ "accountabilityDetails",
+ "accountabilityScore",
+ "approvedAt",
+ "approvedBy",
+ "assessmentDate",
+ "assessmentType",
+ "createdAt",
+ "createdBy",
+ "ethicsDetails",
+ "ethicsScore",
+ "fairnessDetails",
+ "fairnessScore",
+ "orgId",
+ "overallScore",
+ "systemId",
+ "transparencyDetails",
+ "transparencyScore",
+ "updatedAt",
+ "validUntil"
+ ],
+ "spec_only": [
+ "accountability_details",
+ "accountability_score",
+ "approved_at",
+ "approved_by",
+ "assessment_date",
+ "assessment_type",
+ "created_at",
+ "created_by",
+ "ethics_details",
+ "ethics_score",
+ "fairness_details",
+ "fairness_score",
+ "org_id",
+ "overall_score",
+ "rejected_at",
+ "rejected_by",
+ "rejection_reason",
+ "submitted_at",
+ "submitted_by",
+ "system_id",
+ "transparency_details",
+ "transparency_score",
+ "updated_at",
+ "valid_until",
+ "version"
+ ]
+ },
+ "Finding": {
+ "sdk_only": [
+ "dueDate",
+ "pillar"
+ ],
+ "spec_only": [
+ "article"
+ ]
+ },
"HITLApprovalRequest": {
"sdk_only": [],
"spec_only": [
@@ -359,6 +458,13 @@
"user"
]
},
+ "KillSwitch": {
+ "sdk_only": [],
+ "spec_only": [
+ "restore_reason",
+ "trigger_conditions"
+ ]
+ },
"ListWorkflowsResponse": {
"sdk_only": [],
"spec_only": [
@@ -488,6 +594,25 @@
"snapshot"
]
},
+ "RegistrySummary": {
+ "sdk_only": [
+ "by_status",
+ "by_use_case"
+ ],
+ "spec_only": []
+ },
+ "RejectAssessmentRequest": {
+ "sdk_only": [
+ "rejectedBy"
+ ],
+ "spec_only": []
+ },
+ "RestoreKillSwitchRequest": {
+ "sdk_only": [
+ "restoredBy"
+ ],
+ "spec_only": []
+ },
"ResumePlanResponse": {
"sdk_only": [],
"spec_only": [
@@ -516,6 +641,12 @@
"decision_id"
]
},
+ "TriggerKillSwitchRequest": {
+ "sdk_only": [
+ "triggeredBy"
+ ],
+ "spec_only": []
+ },
"UnifiedStepStatus": {
"sdk_only": [
"approvalStatus",
@@ -539,6 +670,28 @@
"tokens_out"
]
},
+ "UpdateAssessmentRequest": {
+ "sdk_only": [
+ "accountabilityDetails",
+ "accountabilityScore",
+ "ethicsDetails",
+ "ethicsScore",
+ "fairnessDetails",
+ "fairnessScore",
+ "transparencyDetails",
+ "transparencyScore"
+ ],
+ "spec_only": [
+ "accountability_details",
+ "accountability_score",
+ "ethics_details",
+ "ethics_score",
+ "fairness_details",
+ "fairness_score",
+ "transparency_details",
+ "transparency_score"
+ ]
+ },
"UpdatePlanRequest": {
"sdk_only": [],
"spec_only": [