This is an automated email from the ASF dual-hosted git repository.

luigidemasi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 4f6e6e25f16b73648e081afdc04277d1ed5efcf0
Author: Luigi De Masi <[email protected]>
AuthorDate: Sat Oct 10 12:40:55 2026 +0200

    CAMEL-25499: Make semantic CLI output readable by default
    
    Render expert contracts, evaluation details and audit evidence as labelled 
text and tables. Keep machine-readable output behind --json, show an unset 
default expert neutrally, and display retained input including multiline and 
structured values.
    
    Co-authored-by: OpenAI Codex <[email protected]>
---
 .../modules/ROOT/pages/camel-jbang-managing.adoc   | 20 ++++-
 .../jbang/core/commands/action/CamelSemantic.java  | 94 +++++++++++++++++++---
 .../commands/action/SemanticActionCommand.java     | 43 ++++++++++
 .../jbang/core/commands/action/SemanticAudit.java  | 82 +++++++++++++++++--
 .../core/commands/action/SemanticEvaluate.java     |  3 +-
 .../core/commands/action/SemanticCommandTest.java  | 71 ++++++++++++----
 6 files changed, 280 insertions(+), 33 deletions(-)

diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
index ac9b0e980060..feff32ebce68 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
@@ -298,7 +298,9 @@ camel semantic get my-app --expert=guard
 
 `camel semantic` defaults to `camel semantic get`. The text output uses tables 
listing
 definition names, experts, operations, state selectors, result types and 
resolution
-errors, followed by the known experts and default expert. `--expert`
+errors, followed by the known experts and default expert. Listing does not 
require a
+default expert: the text output shows `Default expert: none` when no default 
can be
+resolved. `--json` retains the detailed default-selection diagnostics. 
`--expert`
 shows one expert's operations, input and result semantics, and parameter 
contract.
 Inspection does not perform inference. Use `--json` for machine-readable 
output.
 
@@ -344,6 +346,11 @@ reply (default: 60000 milliseconds). A CLI timeout removes 
its request and asks
 connector to cancel unfinished work. Provider interruption is cooperative; 
remote or
 native inference may continue. Configure provider-specific request timeouts as 
well.
 
+Semantic commands use readable text by default: expert operations include a 
parameter table,
+evaluation results use labelled fields, and audit events show the record and 
linked evidence
+in separate sections. Add `--json` to any semantic command to receive the 
complete JSON response
+for scripts.
+
 All semantic commands accept an integration name or PID. With no name they 
select the only
 running integration; ambiguous selection fails and lists the matching PIDs.
 Known versions older than 4.23 fail immediately with exit code 3. If the 
runtime version
@@ -357,6 +364,11 @@ Use `camel semantic audit` to retrieve retained history 
from the application's c
 audit reader. It uses the local connector and performs no inference. Capture 
settings
 remain in the semantic DSL; querying does not enable auditing or change its 
configuration.
 
+Stored input appears in event details and linked evaluation evidence when the 
expert's
+semantic audit configuration explicitly enables `input.enabled`. Capture is 
disabled by
+default, bounded by `maxChars`, and can use a redactor. Human-readable output 
preserves
+multiline text; `--json` returns the stored structure and any omission reason.
+
 [source,bash]
 ----
 camel semantic audit my-app
@@ -367,8 +379,10 @@ camel semantic audit my-app --event-id=EVENT_ID --json
 
 The default output is a table of event IDs, timestamps, categories, explicit 
route actions,
 evaluation statuses, experts, operations, targets, namespaces, reason codes 
and correlation
-IDs. It also reports audit backend status, dropped records and evictions. An 
evaluation
-status is separate from an application's allow/block decision.
+IDs. A compact text summary reports whether capture is enabled, the reader, 
memory
+retention and dropped records. Sink or observer failures appear as separate 
error counts;
+evictions are shown below the table. An evaluation status is separate from an 
application's
+allow/block decision. The full audit configuration and health fields are 
available with `--json`.
 
 `--json` returns the complete runtime response as one JSON document. 
`--event-id` selects
 one historical record and its linked evaluation evidence, including any 
retained provider,
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSemantic.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSemantic.java
index 235148202c20..8dfb3d30ff16 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSemantic.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSemantic.java
@@ -16,16 +16,17 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.action;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.stream.IntStream;
 
 import com.github.freva.asciitable.AsciiTable;
 import com.github.freva.asciitable.Column;
 import com.github.freva.asciitable.HorizontalAlign;
+import com.github.freva.asciitable.OverflowBehaviour;
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
-import org.apache.camel.util.json.Jsoner;
 import picocli.CommandLine;
 
 @CommandLine.Command(name = "semantic", description = "List semantic 
definitions and expert contracts",
@@ -94,10 +95,8 @@ public class CamelSemantic extends SemanticActionCommand {
                 new 
Column().header("REFERENCE").dataAlign(HorizontalAlign.LEFT).with(r -> text(r, 
"reference")),
                 new 
Column().header("NAME").dataAlign(HorizontalAlign.LEFT).with(r -> text(r, 
"name")),
                 new 
Column().header("ERROR").dataAlign(HorizontalAlign.LEFT).with(r -> text(r, 
"error")))));
-        printer().println("Default expert: " + text(response, 
"defaultExpert"));
-        if (response.get("defaultError") != null) {
-            printer().println("Default expert error: " + 
response.get("defaultError"));
-        }
+        String defaultExpert = text(response, "defaultExpert");
+        printer().println("Default expert: " + (defaultExpert.isEmpty() ? 
"none" : defaultExpert));
     }
 
     private static List<JsonObject> rows(JsonArray values) {
@@ -105,15 +104,92 @@ public class CamelSemantic extends SemanticActionCommand {
     }
 
     private void renderOperations(JsonArray operations) {
-        for (int i = 0; i < operations.size(); i++) {
-            JsonObject operation = operations.getMap(i);
+        for (JsonObject operation : rows(operations)) {
+            printer().println();
             printer().println(text(operation, "name") + " (" + text(operation, 
"resultType") + "): "
                               + text(operation, "description"));
-            
printer().println(Jsoner.prettyPrint(Jsoner.serialize(operation.get("contract"))));
+            JsonObject contract = operation.getMap("contract");
+            if (contract == null) {
+                continue;
+            }
+            renderField("Input", contract.get("inputTypes"), "  ");
+            renderField("Requirements", contract.get("inputRequirements"), "  
");
+            renderField("Result", contract.get("resultMeaning"), "  ");
+            renderField("Labels", contract.get("labels"), "  ");
+            renderField("Range", range(contract, "minimum", "maximum"), "  ");
+            renderField("Score levels", contract.get("scoreLevelsParameter"), 
"  ");
+            for (String metric : List.of("probability", "probabilities", 
"confidence")) {
+                if (Boolean.TRUE.equals(contract.get(metric))) {
+                    String meaning = text(contract, 
"confidence".equals(metric) ? "confidenceMeaning" : "probabilityMeaning");
+                    renderField(fieldLabel(metric), meaning.isEmpty() ? 
"available" : meaning, "  ");
+                }
+            }
+            List<JsonObject> parameters = 
rows(contract.getCollection("parameters"));
+            if (!parameters.isEmpty()) {
+                printer().println();
+                printer().println("Parameters:");
+                printer().println(AsciiTable.getTable(AsciiTable.NO_BORDERS, 
parameters, List.of(
+                        new 
Column().header("PARAMETER").dataAlign(HorizontalAlign.LEFT).maxWidth(24, 
OverflowBehaviour.NEWLINE)
+                                .with((JsonObject p) -> text(p, "name")),
+                        new 
Column().header("TYPE").dataAlign(HorizontalAlign.LEFT).maxWidth(16, 
OverflowBehaviour.NEWLINE)
+                                .with(CamelSemantic::parameterType),
+                        new 
Column().header("REQUIRED").dataAlign(HorizontalAlign.LEFT)
+                                .with((JsonObject p) -> 
Boolean.TRUE.equals(p.get("required")) ? "yes" : "no"),
+                        new 
Column().header("DETAILS").dataAlign(HorizontalAlign.LEFT).maxWidth(64, 
OverflowBehaviour.NEWLINE)
+                                .with(CamelSemantic::parameterDetails))));
+            }
+        }
+    }
+
+    private static String parameterType(JsonObject parameter) {
+        String type = text(parameter, "type");
+        if ("List".equals(type) || "Map".equals(type)) {
+            String itemType = text(parameter, "itemType");
+            if (!itemType.isEmpty() && !"Object".equals(itemType)) {
+                type += "<" + itemType + ">";
+            }
+        }
+        return Boolean.TRUE.equals(parameter.get("integer")) ? type + " 
(integer)" : type;
+    }
+
+    private static String parameterDetails(JsonObject parameter) {
+        List<String> details = new ArrayList<>();
+        if (!text(parameter, "description").isEmpty()) {
+            details.add(text(parameter, "description"));
+        }
+        if (!text(parameter, "omission").isEmpty()) {
+            details.add("When omitted: " + text(parameter, "omission"));
+        }
+        JsonArray values = parameter.getCollection("values");
+        if (values != null && !values.isEmpty()) {
+            details.add("Allowed: " + String.join(", ", 
values.stream().map(SemanticActionCommand::displayText).toList()));
+        }
+        String range = range(parameter, "minimum", "maximum");
+        if (!range.isEmpty()) {
+            details.add("Range: " + range);
+        }
+        // The schema's unbounded collection limits are implementation 
defaults, not useful constraints.
+        Object minimum = parameter.get("minSize");
+        Object maximum = parameter.get("maxSize");
+        if (minimum instanceof Number number && number.longValue() > 0) {
+            details.add("Minimum items: " + minimum);
+        }
+        if (maximum instanceof Number number && number.longValue() < 
Integer.MAX_VALUE) {
+            details.add("Maximum items: " + maximum);
+        }
+        return String.join("\n", details);
+    }
+
+    private static String range(JsonObject value, String minimum, String 
maximum) {
+        String min = text(value, minimum);
+        String max = text(value, maximum);
+        if (min.isEmpty()) {
+            return max.isEmpty() ? "" : "<= " + max;
         }
+        return max.isEmpty() ? ">= " + min : min + " to " + max;
     }
 
     private static String text(JsonObject value, String key) {
-        return value.get(key) == null ? "" : value.get(key).toString();
+        return displayText(value.get(key));
     }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticActionCommand.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticActionCommand.java
index 6f0f9dff6d3b..5d447044bf10 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticActionCommand.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticActionCommand.java
@@ -17,7 +17,10 @@
 package org.apache.camel.dsl.jbang.core.commands.action;
 
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
 
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import 
org.apache.camel.dsl.jbang.core.commands.exceptionhandler.UsageErrorHandler;
@@ -135,6 +138,46 @@ public abstract class SemanticActionCommand extends 
ActionBaseCommand implements
         return 0;
     }
 
+    protected void renderField(String label, Object value, String indent) {
+        if (value == null) {
+            return;
+        }
+        if (value instanceof Map<?, ?> fields) {
+            if (!fields.isEmpty()) {
+                printer().println(indent + label + ":");
+                fields.forEach((key, item) -> renderField(displayText(key), 
item, indent + "  "));
+            }
+        } else if (value instanceof Collection<?> items) {
+            if (items.stream().anyMatch(item -> item instanceof Map || item 
instanceof Collection)) {
+                int index = 1;
+                for (Object item : items) {
+                    renderField(label + " " + index++, item, indent);
+                }
+            } else if (!items.isEmpty()) {
+                printer().println(indent + label + ": "
+                                  + 
items.stream().map(SemanticActionCommand::displayText).collect(Collectors.joining(",
 ")));
+            }
+        } else if (!displayText(value).isEmpty()) {
+            printer().println(indent + label + ": " + displayText(value));
+        }
+    }
+
+    protected static String displayText(Object value) {
+        // Keep provider text from introducing terminal control sequences in 
human-readable output.
+        return value == null ? "" : 
value.toString().replaceAll("[\\p{Cntrl}\\p{Cf}]", " ");
+    }
+
+    protected static String fieldLabel(String key) {
+        if ("elapsedMillis".equals(key)) {
+            return "Elapsed (ms)";
+        }
+        if ("durationNanos".equals(key)) {
+            return "Duration (ns)";
+        }
+        String label = key.replaceAll("([a-z0-9])([A-Z])", "$1 $2");
+        return Character.toUpperCase(label.charAt(0)) + 
label.substring(1).replace(" Id", " ID");
+    }
+
     protected abstract JsonObject request();
 
     protected abstract void render(JsonObject response);
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticAudit.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticAudit.java
index feeac68567c8..9ebf24eeb0f6 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticAudit.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticAudit.java
@@ -19,6 +19,8 @@ package org.apache.camel.dsl.jbang.core.commands.action;
 import java.time.Instant;
 import java.time.format.DateTimeParseException;
 import java.util.List;
+import java.util.Map;
+import java.util.StringJoiner;
 import java.util.stream.IntStream;
 
 import com.github.freva.asciitable.AsciiTable;
@@ -28,7 +30,6 @@ import com.github.freva.asciitable.HorizontalAlign;
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
-import org.apache.camel.util.json.Jsoner;
 import picocli.CommandLine;
 
 @CommandLine.Command(name = "audit", description = "Retrieve retained semantic 
audit records and linked evidence",
@@ -131,6 +132,34 @@ public class SemanticAudit extends SemanticActionCommand {
         return super.printResponse(response);
     }
 
+    private void renderAuditField(String key, Object value, String indent) {
+        if ("input".equals(key)) {
+            renderInput("Input", value, indent);
+        } else {
+            renderField(fieldLabel(key), value, indent);
+        }
+    }
+
+    private void renderInput(String label, Object value, String indent) {
+        if (value instanceof Map<?, ?> map && !map.isEmpty()) {
+            printer().println(indent + label + ":");
+            map.forEach((key, item) -> renderInput(displayText(key), item, 
indent + "  "));
+        } else if (value instanceof List<?> list && !list.isEmpty()) {
+            printer().println(indent + label + ":");
+            for (int i = 0; i < list.size(); i++) {
+                renderInput("[" + i + "]", list.get(i), indent + "  ");
+            }
+        } else if (value instanceof String text && text.contains("\n")) {
+            printer().println(indent + label + ":");
+            for (String line : text.split("\\R", -1)) {
+                printer().println(indent + "  " + displayText(line));
+            }
+        } else {
+            printer()
+                    .println(indent + label + ": " + (value == null ? "null" : 
"".equals(value) ? "\"\"" : displayText(value)));
+        }
+    }
+
     @Override
     protected void render(JsonObject response) {
         if (response.containsKey("error")) {
@@ -138,15 +167,30 @@ public class SemanticAudit extends SemanticActionCommand {
         }
         JsonObject audit = response.getMap("audit");
         if (audit != null) {
-            printer().println("Audit: " + audit.toJson());
+            renderStatus(audit);
         }
         if (eventId != null) {
-            Object record = response.get("record");
-            printer().println(record == null ? "Record unavailable." : 
Jsoner.prettyPrint(Jsoner.serialize(record)));
+            JsonObject record = response.getMap("record");
+            if (record == null) {
+                printer().println("Record unavailable.");
+            } else {
+                printer().println();
+                printer().println("Record:");
+                record.forEach((key, value) -> renderAuditField(key, value, "  
"));
+            }
             JsonArray evidence = response.getCollection("evidence");
             if (evidence != null && !evidence.isEmpty()) {
+                printer().println();
                 printer().println("Evidence:");
-                printer().println(Jsoner.prettyPrint(evidence.toJson()));
+                for (int i = 0; i < evidence.size(); i++) {
+                    JsonObject linked = evidence.getMap(i);
+                    printer().println("  " + 
displayText(linked.get("eventId")));
+                    linked.forEach((key, value) -> {
+                        if (!"eventId".equals(key)) {
+                            renderAuditField(key, value, "    ");
+                        }
+                    });
+                }
             }
             return;
         }
@@ -169,6 +213,34 @@ public class SemanticAudit extends SemanticActionCommand {
         }
     }
 
+    private void renderStatus(JsonObject audit) {
+        Object capturing = audit.getOrDefault("decisionsEnabled", 
audit.get("enabled"));
+        String state = capturing instanceof Boolean enabled ? (enabled ? 
"enabled" : "disabled") : "unknown";
+        StringJoiner summary = new StringJoiner(" | ");
+        summary.add("Audit: " + state);
+        if (audit.get("reader") != null) {
+            summary.add("Reader: " + audit.get("reader"));
+        }
+        if ("memory".equals(audit.get("reader")) && audit.get("retained") != 
null && audit.get("capacity") != null) {
+            summary.add("Retained: " + audit.get("retained") + "/" + 
audit.get("capacity"));
+        }
+        if (audit.get("dropped") != null) {
+            summary.add("Dropped: " + audit.get("dropped"));
+        }
+        printer().println(summary.toString());
+        JsonObject sinkErrors = audit.getMap("sinkErrors");
+        if (sinkErrors != null) {
+            sinkErrors.forEach((sink, count) -> {
+                if (count instanceof Number number && number.longValue() > 0) {
+                    printer().println("Sink errors (" + sink + "): " + count);
+                }
+            });
+        }
+        if (audit.get("observerErrors") instanceof Number errors && 
errors.longValue() > 0) {
+            printer().println("Observer errors: " + errors);
+        }
+    }
+
     private static ColumnData<JsonObject> column(String heading, String field) 
{
         return new Column().header(heading).dataAlign(HorizontalAlign.LEFT)
                 .with((JsonObject row) -> row.get(field) == null ? "" : 
row.get(field).toString());
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticEvaluate.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticEvaluate.java
index cee694a82653..481aa5f9eb13 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticEvaluate.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticEvaluate.java
@@ -26,7 +26,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.ObjectReader;
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.util.json.JsonObject;
-import org.apache.camel.util.json.Jsoner;
 import picocli.CommandLine;
 
 @CommandLine.Command(name = "eval", description = "Evaluate a semantic 
definition or expert operation",
@@ -169,7 +168,7 @@ public class SemanticEvaluate extends SemanticActionCommand 
{
         for (String key : List.of("evaluation", "status", "value", 
"probability", "probabilities", "confidence",
                 "metadata", "elapsedMillis", "error")) {
             if (response.containsKey(key)) {
-                printer().println(key + ": " + 
Jsoner.serialize(response.get(key)));
+                renderField(fieldLabel(key), response.get(key), "");
             }
         }
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticCommandTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticCommandTest.java
index 0dda78737cd6..b8f667bc9a07 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticCommandTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticCommandTest.java
@@ -22,7 +22,9 @@ import java.io.StringWriter;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -87,8 +89,9 @@ class SemanticCommandTest {
         JsonObject response = json("""
                 
{"evaluations":[{"name":"safe","expert":"guard","operation":"detect",
                 "state":"${body}","resultType":"boolean","error":"definition 
error"}],
-                "experts":[{"reference":"guard","name":"Safety 
guard","error":"expert error"}],
-                "defaultError":"No default expert"}
+                "experts":[{"reference":"guard","name":"Safety 
guard","error":"expert error"},
+                {"reference":"classifier","name":"Classifier"}],
+                "defaultError":"Semantic language requires exactly one 
eligible expert"}
                 """);
         AtomicReference<JsonObject> request = new AtomicReference<>();
         assertEquals(0, respond(new CamelSemantic(main()), r -> {
@@ -103,7 +106,10 @@ class SemanticCommandTest {
         assertFalse(printer.getOutput().contains("\t"));
         assertTrue(printer.getOutput().contains("Safety guard"));
         assertTrue(printer.getOutput().contains("expert error"));
-        assertTrue(printer.getOutput().contains("No default expert"));
+        assertTrue(printer.getOutput().contains("Classifier"));
+        assertTrue(printer.getOutput().contains("Default expert: none"));
+        assertFalse(printer.getOutput().contains("Default expert error:"));
+        assertFalse(printer.getOutput().contains("requires exactly one 
eligible expert"));
         assertEquals("", errors.toString());
     }
 
@@ -115,21 +121,33 @@ class SemanticCommandTest {
         assertEquals("", errors.toString());
     }
 
-    @Test
-    void showsExpertOperationAndParameterContract() throws Exception {
+    @ParameterizedTest
+    @ValueSource(booleans = { false, true })
+    void showsExpertOperationAndParameterContract(boolean jsonOutput) throws 
Exception {
         JsonObject response = json("""
                 {"operations":[{"name":"rank","description":"Rank 
text","resultType":"score",
                 
"contract":{"inputTypes":["text"],"parameters":[{"name":"criteria","type":"List",
-                
"itemType":"String","required":true}],"resultMeaning":"quality"}}]}
+                "itemType":"String","required":true,"description":"Ordered 
levels","minSize":2,"maxSize":10},
+                
{"name":"threshold","type":"Number","minimum":0,"maximum":1,"omission":"Expert 
default"}],
+                
"minimum":0,"maximum":1,"resultMeaning":"quality","probability":true,
+                "probabilityMeaning":"Likelihood","confidence":false}}]}
                 """);
         assertEquals(0, respond(new CamelSemantic(main()), r -> {
             assertEquals("grader", r.get("expert"));
             assertFalse(r.containsKey("overview"));
             return response;
-        }, "--expert=grader"));
-        assertTrue(printer.getOutput().contains("rank (score): Rank text"));
-        assertTrue(printer.getOutput().contains("criteria"));
-        assertTrue(printer.getOutput().contains("quality"));
+        }, "--expert=grader", "--json=" + jsonOutput));
+        if (jsonOutput) {
+            assertEquals(response, json(printer.getOutput()));
+        } else {
+            for (String value : List.of("rank (score): Rank text", "Input: 
text", "Result: quality", "Range: 0 to 1",
+                    "Probability: Likelihood", "PARAMETER", "TYPE", 
"REQUIRED", "DETAILS", "criteria", "List<String>",
+                    "yes", "Ordered levels", "Minimum items: 2", "Maximum 
items: 10", "When omitted: Expert default")) {
+                assertTrue(printer.getOutput().contains(value), 
printer.getOutput());
+            }
+            assertFalse(printer.getOutput().contains("\"inputTypes\""));
+            assertFalse(printer.getOutput().contains("Confidence:"));
+        }
     }
 
     @Test
@@ -201,7 +219,7 @@ class SemanticCommandTest {
         }, "--expert=grader", "--operation=rank", 
"--input=json:[\"one\",\"two\"]",
                 "--parameter=criteria=json:[\"poor\",\"good\"]", 
"--parameter=threshold=json:0.7",
                 "--parameter=strict=json:true", "--parameter=description=plain 
text"));
-        for (String key : List.of("value", "probability", "probabilities", 
"confidence", "elapsedMillis")) {
+        for (String key : List.of("Value", "Probability", "Probabilities", 
"Confidence", "Elapsed (ms)")) {
             assertTrue(printer.getOutput().contains(key + ":"));
         }
     }
@@ -486,7 +504,8 @@ class SemanticCommandTest {
             assertEquals(50L, request.getLong("limit"));
             assertEquals(2, request.size());
             return jsonUnchecked("""
-                    {"audit":{"reader":"memory","enabled":true,"dropped":2},
+                    
{"audit":{"reader":"memory","enabled":false,"decisionsEnabled":true,
+                      
"retained":9,"capacity":1000,"dropped":2,"sinkErrors":{"memory":0,"log":3},"observerErrors":1},
                      
"records":[{"eventId":"decision-1","timestamp":"2026-10-10T08:00:00Z",
                        
"category":"decision","action":"block","operation":"tools/call","target":"support-request",
                        
"namespace":"test","reasonCode":"policy_denied","correlationId":"request-123"},
@@ -497,10 +516,13 @@ class SemanticCommandTest {
         }));
         for (String value : List.of("EVENT ID", "TIMESTAMP", "ACTION", 
"STATUS", "decision-1", "evaluation-1",
                 "block", "success", "guard", "tools/call", "support-request", 
"policy_denied", "request-123",
-                "Next cursor: opaque:cursor", "Evicted: 7", "\"dropped\":2")) {
+                "Next cursor: opaque:cursor", "Evicted: 7", "Audit: enabled | 
Reader: memory | Retained: 9/1000 | Dropped: 2",
+                "Sink errors (log): 3", "Observer errors: 1")) {
             assertTrue(printer.getOutput().contains(value), 
printer.getOutput());
         }
         assertFalse(printer.getOutput().contains("allow"));
+        assertFalse(printer.getOutput().contains("Audit: {"));
+        assertFalse(printer.getOutput().contains("Sink errors (memory)"));
         assertEquals("", errors.toString());
     }
 
@@ -536,6 +558,7 @@ class SemanticCommandTest {
                  
"record":{"eventId":"decision-1","category":"decision","action":"block",
                            "evidence":["evaluation-1","evicted-1"]},
                  
"evidence":[{"eventId":"evaluation-1","provider":"safety-provider","model":"guard-v1","revision":"r1",
+                              "input":{"prompt":"stored 
prompt"},"inputRedacted":true,
                               
"result":{"value":false},"semantics":{"meaning":"Injection detected"}},
                              {"eventId":"evicted-1","unavailable":true}]}
                 """);
@@ -549,13 +572,32 @@ class SemanticCommandTest {
             assertEquals(response, json(printer.getOutput()));
         } else {
             for (String value : List.of("decision-1", "Evidence:", 
"evaluation-1", "safety-provider", "guard-v1",
-                    "Injection detected", "evicted-1", "\"unavailable\": 
true")) {
+                    "Injection detected", "Input:", "prompt: stored prompt", 
"Input Redacted: true", "evicted-1",
+                    "Unavailable: true")) {
                 assertTrue(printer.getOutput().contains(value), 
printer.getOutput());
             }
         }
         assertEquals("", errors.toString());
     }
 
+    @Test
+    void auditShowsMultilineAndEmptyStoredInputWithoutTerminalControls() 
throws Exception {
+        JsonObject record = new JsonObject();
+        record.put("eventId", "evaluation-1");
+        Map<String, Object> input = new LinkedHashMap<>();
+        input.put("text", "hello\nworld\u001b[31m");
+        input.put("empty", "");
+        input.put("missing", null);
+        input.put("items", List.of());
+        record.put("input", input);
+        assertEquals(0, respond(new SemanticAudit(main()), request -> new 
JsonObject(Map.of("record", record)),
+                "--event-id=evaluation-1"));
+        for (String value : List.of("text:\n", "hello\n", "world", "empty: 
\"\"", "missing: null", "items: []")) {
+            assertTrue(printer.getOutput().contains(value), 
printer.getOutput());
+        }
+        assertFalse(printer.getOutput().contains("\u001b"));
+    }
+
     @ParameterizedTest
     @ValueSource(ints = { 1, 200 })
     void auditAcceptsPageLimitBoundsAndEmptyHistory(int limit) throws 
Exception {
@@ -566,6 +608,7 @@ class SemanticCommandTest {
                     """);
         }, "--limit=" + limit));
         assertTrue(printer.getOutput().contains("No retained audit records 
match this query."));
+        assertTrue(printer.getOutput().contains("Audit: disabled | Reader: 
memory"));
         assertEquals("", errors.toString());
     }
 

Reply via email to