Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)`)
Expand All @@ -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
Expand Down
112 changes: 112 additions & 0 deletions runtime-e2e/masfeat_registry_summary/MasfeatRegistrySummaryTest.java
Original file line number Diff line number Diff line change
@@ -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");
}
}
35 changes: 35 additions & 0 deletions runtime-e2e/masfeat_registry_summary/README.md
Original file line number Diff line number Diff line change
@@ -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
```
33 changes: 30 additions & 3 deletions scripts/wire_shape/AuditWireKeysProbe.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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
Expand Down Expand Up @@ -90,21 +94,31 @@ public static void main(String[] args) {
}
try {
ObjectMapper mapper = productionConfiguredMapper();
TreeMap<String, TreeSet<String>> result = new TreeMap<>();
TreeMap<String, TreeMap<String, TreeSet<String>>> result = new TreeMap<>();
for (String fqcn : args) {
Class<?> cls = Class.forName(fqcn);
refuseUnintrospectableMechanisms(cls);
JavaType type = mapper.constructType(cls);
TreeSet<String> keys = new TreeSet<>();
TreeSet<String> deprecated = new TreeSet<>();
BeanDescription ser = mapper.getSerializationConfig().introspect(type);
for (BeanPropertyDefinition p : ser.findProperties()) {
keys.add(p.getName());
if (isDeprecated(p)) {
deprecated.add(p.getName());
}
}
BeanDescription deser = mapper.getDeserializationConfig().introspect(type);
for (BeanPropertyDefinition p : deser.findProperties()) {
keys.add(p.getName());
if (isDeprecated(p)) {
deprecated.add(p.getName());
}
}
result.put(cls.getSimpleName(), keys);
TreeMap<String, TreeSet<String>> entry = new TreeMap<>();
entry.put("keys", keys);
entry.put("deprecated", deprecated);
result.put(cls.getSimpleName(), entry);
}
System.out.println(mapper.writeValueAsString(result));
} catch (Throwable t) {
Expand All @@ -128,6 +142,19 @@ private static ObjectMapper productionConfiguredMapper() throws Exception {
return (ObjectMapper) factory.invoke(null);
}

/**
* A property is deprecated if ANY of its backing members (field, getter, setter, creator
* parameter) carries {@code @Deprecated}. {@code java.lang.Deprecated} has runtime retention,
* so the compiled classes carry it.
*/
private static boolean isDeprecated(BeanPropertyDefinition p) {
return (p.getField() != null && p.getField().getAnnotation(Deprecated.class) != null)
|| (p.getGetter() != null && p.getGetter().getAnnotation(Deprecated.class) != null)
|| (p.getSetter() != null && p.getSetter().getAnnotation(Deprecated.class) != null)
|| (p.getConstructorParameter() != null
&& p.getConstructorParameter().getAnnotation(Deprecated.class) != null);
}

private static void refuseUnintrospectableMechanisms(Class<?> cls) {
for (Class<?> c = cls; c != null && c != Object.class; c = c.getSuperclass()) {
refuse(cls, c, "class " + c.getSimpleName(), REFUSED_CLASS_ANNOTATIONS);
Expand Down
Loading
Loading