From ed88a6ae3dc5523d5908c6d3f225a0e6c73c6b09 Mon Sep 17 00:00:00 2001
From: Vishnu R
Date: Wed, 23 Sep 2026 10:45:47 +0530
Subject: [PATCH 1/4] feat: emit the Node validation object from Spring MVC
Spring already reads the live request mappings and an exact OpenAPI document. The snapshot now carries the same disagreement findings, including acknowledgement that keeps the route in the runtime record.
Signed-off-by: Vishnu R
---
CHANGELOG.md | 2 +
README.md | 2 +-
.../flexdoc/spring/FlexDocProperties.java | 30 ++-
.../spring/SpringContractValidation.java | 190 ++++++++++++++++++
.../spring/SpringRuntimeIntelligence.java | 31 ++-
.../spring/SpringRuntimeIntelligenceTest.java | 62 ++++++
docs/runtime-intelligence.md | 4 +-
examples/java-spring/README.md | 19 +-
.../example/FlexDocExampleApplication.java | 8 +-
.../src/main/resources/application.properties | 2 +
10 files changed, 339 insertions(+), 11 deletions(-)
create mode 100644 adapters/java-spring/src/main/java/com/prauga/flexdoc/spring/SpringContractValidation.java
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2231984d..3006d36d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,8 @@ Notable FlexDoc product releases are recorded here. Detailed release notes remai
## Unreleased
+- Spring MVC emits the same runtime-contract validation object as Node. A complete-discovery undocumented route fails `flexdoc validate` by method and path. `flexdoc.acknowledged-undocumented` keeps that route registered as acknowledged info.
+
## 3.5.0 — Node disagreement loop
`@prauga/flexdoc-client` and `@prauga/flexdoc-backend` **3.5.0**, with `@prauga/flexdoc-cli` **0.8.0**. Native adapter versions are unchanged.
diff --git a/README.md b/README.md
index 7295c72a..4afaa35d 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@
-FlexDoc runs inside the backend and reads the routes the framework actually registered. **On Node today,** it compares that runtime surface with OpenAPI, fails `flexdoc validate` on unacknowledged disagreement, and lets you open an undocumented runtime route directly in the built-in API Client.
+FlexDoc runs inside the backend and reads the routes the framework actually registered. **On Node and Spring,** it compares that runtime surface with OpenAPI, fails `flexdoc validate` on unacknowledged disagreement, and lets you open an undocumented runtime route directly in the built-in API Client.
It also provides self-hosted API documentation, exploration, and execution. No FlexDoc account or control plane required.
diff --git a/adapters/java-spring/src/main/java/com/prauga/flexdoc/spring/FlexDocProperties.java b/adapters/java-spring/src/main/java/com/prauga/flexdoc/spring/FlexDocProperties.java
index 54bd7869..04cda930 100644
--- a/adapters/java-spring/src/main/java/com/prauga/flexdoc/spring/FlexDocProperties.java
+++ b/adapters/java-spring/src/main/java/com/prauga/flexdoc/spring/FlexDocProperties.java
@@ -41,8 +41,28 @@ public class FlexDocProperties {
private int hostExecutionMaxInFlight = 16;
/** Retry-After seconds returned when process-local host-execution admission is saturated. */
private int hostExecutionRetryAfterSeconds = 1;
- /** Enables live Spring route discovery and OpenAPI presence drift reporting. */
+ /** Enables live Spring route discovery and the same contract-validation object Node emits. */
private boolean runtimeIntelligence;
+ /** Runtime routes accepted as intentionally absent from OpenAPI. They stay registered. */
+ private List acknowledgedUndocumented = List.of();
+
+ /** One runtime route the operator has accepted as undocumented. */
+ public static class AcknowledgedRoute {
+ private String method = "";
+ private String path = "";
+
+ /** @return the HTTP method */
+ public String getMethod() { return method; }
+
+ /** @param method the HTTP method */
+ public void setMethod(String method) { this.method = method; }
+
+ /** @return the route path */
+ public String getPath() { return path; }
+
+ /** @param path the route path */
+ public void setPath(String path) { this.path = path; }
+ }
/** @return whether FlexDoc auto-configuration is enabled */
public boolean isEnabled() { return enabled; }
@@ -158,6 +178,14 @@ public void setHostExecutionRetryAfterSeconds(int hostExecutionRetryAfterSeconds
/** @param runtimeIntelligence whether Runtime Intelligence is enabled */
public void setRuntimeIntelligence(boolean runtimeIntelligence) { this.runtimeIntelligence = runtimeIntelligence; }
+ /** @return runtime routes accepted as intentionally absent from OpenAPI */
+ public List getAcknowledgedUndocumented() { return acknowledgedUndocumented; }
+
+ /** @param acknowledgedUndocumented runtime routes accepted as intentionally absent from OpenAPI */
+ public void setAcknowledgedUndocumented(List acknowledgedUndocumented) {
+ this.acknowledgedUndocumented = acknowledgedUndocumented == null ? List.of() : List.copyOf(acknowledgedUndocumented);
+ }
+
FlexDocConfig toConfig() {
FlexDocConfig.Builder builder = FlexDocConfig.builder()
.path(path)
diff --git a/adapters/java-spring/src/main/java/com/prauga/flexdoc/spring/SpringContractValidation.java b/adapters/java-spring/src/main/java/com/prauga/flexdoc/spring/SpringContractValidation.java
new file mode 100644
index 00000000..a293b41f
--- /dev/null
+++ b/adapters/java-spring/src/main/java/com/prauga/flexdoc/spring/SpringContractValidation.java
@@ -0,0 +1,190 @@
+package com.prauga.flexdoc.spring;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Ports the Node runtime-contract validator. Path-parameter names are ignored for route identity.
+ * Acknowledged undocumented routes stay registered and are reported as informational findings.
+ */
+final class SpringContractValidation {
+ private SpringContractValidation() {}
+
+ static Map validate(
+ List documentedRoutes,
+ List runtimeRoutes,
+ boolean discoveryComplete,
+ List acknowledgedUndocumented) {
+ Set documentedKeys = new LinkedHashSet<>();
+ for (SpringRuntimeIntelligence.RuntimeRoute route : documentedRoutes) documentedKeys.add(exactShapeKey(route));
+ Set runtimeKeys = new LinkedHashSet<>();
+ for (SpringRuntimeIntelligence.RuntimeRoute route : runtimeRoutes) runtimeKeys.add(exactShapeKey(route));
+ Set acknowledgedKeys = new LinkedHashSet<>();
+ for (SpringRuntimeIntelligence.RuntimeRoute route : acknowledgedUndocumented) acknowledgedKeys.add(exactShapeKey(route));
+
+ List