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

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


The following commit(s) were added to refs/heads/main by this push:
     new 18d98ea5f7f1 CAMEL-26410: camel-jbang - AI tools: a refused write 
reports the problems behind a YAML indentation the write repairs by itself 
(#28717)
18d98ea5f7f1 is described below

commit 18d98ea5f7f121f4d9619b39933826b5939a5fad
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Oct 30 10:46:35 2026 +0100

    CAMEL-26410: camel-jbang - AI tools: a refused write reports the problems 
behind a YAML indentation the write repairs by itself (#28717)
    
    The write and edit tools repair a YAML shape with one reading, such as a 
list
    item two columns off. When the repaired content had another problem, the
    repair was dropped and the write was refused with the indentation error 
only:
    the parser stops there, so the other checks never ran. A local model then 
sent
    the same lines up to 19 times in a row.
    
    Now the write is still refused, but with the problems of the repaired 
content,
    and a note that the write repairs the shape by itself. When a problem of the
    content as written is still there, or a repair moved lines, the answer stays
    as before.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../dsl/jbang/core/commands/ai/AuthoringTools.java |  56 +++++++++-
 .../dsl/jbang/core/commands/ai/LenientRepairs.java |  11 +-
 .../commands/ai/AuthoringToolsLenientTest.java     | 121 +++++++++++++++++++++
 3 files changed, 179 insertions(+), 9 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
index cbbb5be0abae..fd4ce3e0dd1a 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
@@ -35,6 +35,7 @@ import java.util.Collection;
 import java.util.Comparator;
 import java.util.EnumSet;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Locale;
@@ -748,6 +749,10 @@ public final class AuthoringTools {
             result.put("message", "The file was not changed: the result has 
validation errors. Fix them and call"
                                   + " camel_edit_file again.");
         }
+        if ("invalid".equals(result.getString("status")) && 
result.get("repairable") instanceof JsonArray repairable) {
+            result.put("message", result.getString("message") + " "
+                                  + 
repairableNote(repairable.stream().map(String::valueOf).toList()));
+        }
         return result;
     }
 
@@ -1672,6 +1677,7 @@ public final class AuthoringTools {
         }
         List<String> problemsBefore = List.of();
         List<String> repaired = List.of();
+        List<String> repairable = List.of();
         if (validate && SourceValidator.isValidatableFile(file)) {
             // a missing consumer of a direct: endpoint does not refuse the 
write: it is often a file not written yet
             List<String> errors = SourceValidator.validate(file, content, 
ctx.catalog(), ctx.propertyLineValidator(), dir,
@@ -1690,11 +1696,16 @@ public final class AuthoringTools {
                 // CAMEL-25532: the shapes with one reading are repaired, and 
the write goes ahead when that leaves no
                 // new problem; the answer says what was repaired
                 Repaired r = repair(ctx, dir, file, content, errors, before, 
exists);
-                if (r != null) {
+                List<String> hidden = hiddenProblems(r, errors);
+                if (r != null && r.left().isEmpty()) {
                     content = r.content();
                     repaired = r.applied();
                     problemsBefore = r.stillThere();
                     errors = List.of();
+                } else if (hidden != null) {
+                    // the write repairs the shape itself: the problems to fix 
are the ones the shape hid
+                    errors = hidden;
+                    repairable = r.applied();
                 }
             }
             if (!errors.isEmpty()) {
@@ -1714,6 +1725,10 @@ public final class AuthoringTools {
                                           + " it, refused " + times + " times 
now for the same problems. Change what"
                                           + " the errors say; where an error 
shows the lines to write, copy them.");
                 }
+                if (!repairable.isEmpty()) {
+                    result.put("repairable", new JsonArray(repairable));
+                    result.put("message", result.getString("message") + " " + 
repairableNote(repairable));
+                }
                 putKameletGuide(result, file, errors);
                 return result;
             }
@@ -1884,12 +1899,17 @@ public final class AuthoringTools {
     /** How many times the repairs are applied and the content validated again 
(a repair can reveal the next one). */
     private static final int REPAIR_ROUNDS = 8;
 
-    private record Repaired(String content, List<String> applied, List<String> 
stillThere) {
+    /**
+     * The content with the shapes that have one reading repaired; left has 
the problems it still has (none: the write
+     * goes ahead with the repairs), and moved says an entry moved to other 
lines.
+     */
+    private record Repaired(
+            String content, List<String> applied, List<String> stillThere, 
List<String> left, boolean moved) {
     }
 
     /**
-     * The content with the shapes that have one reading repaired, when that 
leaves no problem the file did not have;
-     * null otherwise (the write is refused as it was, with the problems of 
the content as written).
+     * The content with the shapes that have one reading repaired, with the 
problems it still has once repaired; null
+     * when nothing could be repaired (the write is refused with the problems 
of the content as written).
      */
     private static Repaired repair(
             ToolContext ctx, Path dir, String file, String content, 
List<String> errors, List<String> before,
@@ -1898,20 +1918,44 @@ public final class AuthoringTools {
         List<String> applied = new ArrayList<>();
         List<String> left = errors;
         List<String> stillThere = new ArrayList<>();
+        boolean moved = false;
         for (int round = 0; round < REPAIR_ROUNDS && !left.isEmpty(); round++) 
{
             LenientRepairs.Result r = LenientRepairs.repair(current, left);
             if (r == null) {
-                return null;
+                break;
             }
             current = r.content();
             applied.addAll(r.applied());
+            moved |= r.moved();
             left = SourceValidator.validate(file, current, ctx.catalog(), 
ctx.propertyLineValidator(), dir, null, false);
             stillThere = new ArrayList<>();
             if (!left.isEmpty() && exists) {
                 left = newProblems(before, left, stillThere);
             }
         }
-        return left.isEmpty() ? new Repaired(current, applied, stillThere) : 
null;
+        return applied.isEmpty() ? null : new Repaired(current, applied, 
stillThere, left, moved);
+    }
+
+    /**
+     * The problems a repairable shape hid: the YAML parser stops at a list 
item two columns off, so the checks of the
+     * content never ran. Told only of the column, a local model sent the same 
lines up to 22 times in a row (r23 and
+     * r24 of the benchmark), while with the column repaired the validator had 
the real problem: a steps: at the route
+     * level, a field read on a variable that holds a file. Null when a 
problem of the content as written is still there
+     * (the write cannot repair it), or the repairs moved lines, so the line 
numbers would not be the model's.
+     */
+    private static List<String> hiddenProblems(Repaired r, List<String> 
errors) {
+        if (r == null || r.left().isEmpty() || r.moved()) {
+            return null;
+        }
+        Set<String> written = new HashSet<>();
+        errors.forEach(e -> written.add(withoutLine(e)));
+        return r.left().stream().anyMatch(e -> 
written.contains(withoutLine(e))) ? null : r.left();
+    }
+
+    /** What the write repairs by itself, said with the problems it still 
refuses. */
+    static String repairableNote(List<String> repairable) {
+        return "The YAML shape needs no change from you: the write repairs it 
by itself (" + String.join("; ", repairable)
+               + "). The errors are what the content has with that repaired.";
     }
 
     private static final Pattern UNCONSUMED_DIRECT = Pattern.compile("sends to 
(direct:[^,\\s]+), and no route consumes it");
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/LenientRepairs.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/LenientRepairs.java
index 631b162a175f..dfc653aaef76 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/LenientRepairs.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/LenientRepairs.java
@@ -36,8 +36,11 @@ import java.util.regex.Pattern;
  */
 final class LenientRepairs {
 
-    /** The repaired content and what was done, one line each ("line 13: 
steps: indented under split:"). */
-    record Result(String content, List<String> applied) {
+    /**
+     * The repaired content and what was done, one line each ("line 13: steps: 
indented under split:"); moved when an
+     * entry moved to other lines, so the line numbers of the content are not 
the ones the model wrote.
+     */
+    record Result(String content, List<String> applied, boolean moved) {
     }
 
     /** A repair of one place of the content; a move changes the lines above 
it, so it goes alone. */
@@ -125,12 +128,14 @@ final class LenientRepairs {
         todo = 
todo.stream().sorted(Comparator.comparingInt(Repair::line).reversed()).toList();
         Set<Integer> done = new HashSet<>();
         List<String> applied = new ArrayList<>();
+        boolean moved = false;
         for (Repair r : todo) {
             if (done.add(r.line()) && r.action().apply(lines)) {
                 applied.add(0, r.description());
+                moved |= r.move();
             }
         }
-        return applied.isEmpty() ? null : new Result(String.join("\n", lines), 
applied);
+        return applied.isEmpty() ? null : new Result(String.join("\n", lines), 
applied, moved);
     }
 
     private static List<Repair> repairsFor(String error, List<String> lines) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsLenientTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsLenientTest.java
index 7ed31de5a0b3..190087096e95 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsLenientTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsLenientTest.java
@@ -258,6 +258,127 @@ class AuthoringToolsLenientTest {
                 """);
         assertThat(result.getString("status")).isEqualTo("invalid");
         assertThat(result.get("repaired")).isNull();
+        assertThat(result.get("repairable")).isNull();
+    }
+
+    @Test
+    void aListItemTwoColumnsOffDoesNotHideTheProblemBehindIt(@TempDir Path 
dir) throws IOException {
+        // r23l-3, r24l-2 and r24l-6 route-aggregator step 1: the log two 
columns off stopped the parser, so the steps:
+        // at the route level was never reported, and the column alone was 
refused up to 19 times in a row
+        String content = """
+                - route:
+                    id: shipments
+                    from:
+                      uri: direct:shipment
+                    steps:
+                      - aggregate:
+                          aggregationStrategy: 
"#class:org.apache.camel.processor.aggregate.GroupedBodyAggregationStrategy"
+                          correlationExpression:
+                            simple:
+                              expression: "${header.orderId}"
+                          completionSize: 2
+                        - log:
+                            message: "Shipment complete"
+                """;
+        JsonObject result = write(dir, content);
+        
assertThat(result.getString("status")).as(String.valueOf(result)).isEqualTo("invalid");
+        assertThat(result.get("errors").toString()).contains("steps: goes 
under from:")
+                .doesNotContain("every item of a list");
+        assertThat(result.get("repairable").toString()).contains("the list 
item moved to the column of the list");
+        assertThat(result.getString("message")).contains("the write repairs it 
by itself");
+        assertThat(result.get("repaired")).isNull();
+
+        // the steps: under from:, the log still two columns off: the write 
goes ahead with the repair
+        JsonObject again = write(dir, """
+                - route:
+                    id: shipments
+                    from:
+                      uri: direct:shipment
+                      steps:
+                        - aggregate:
+                            aggregationStrategy: 
"#class:org.apache.camel.processor.aggregate.GroupedBodyAggregationStrategy"
+                            correlationExpression:
+                              simple:
+                                expression: "${header.orderId}"
+                            completionSize: 2
+                          - log:
+                              message: "Shipment complete"
+                """);
+        assertRepairedAndValid(dir, again, "the list item moved to the column 
of the list");
+    }
+
+    @Test
+    void aListItemTwoColumnsOffDoesNotHideABodyTypeProblem(@TempDir Path dir) {
+        // r24n-2 route-enrich-and-dedupe step 2: the log after a pollEnrich 
two columns off, refused 16 times in a row,
+        // hid that the log reads a field of the file that the pollEnrich put 
in a variable
+        JsonObject result = write(dir, """
+                - route:
+                    from:
+                      uri: file:orders
+                      steps:
+                        - unmarshal:
+                            json: {}
+                        - pollEnrich:
+                            expression:
+                              simple:
+                                expression: 
"file:customers?fileName=${body[customer]}.json&noop=true"
+                            timeout: 5000
+                            variableReceive: customerData
+                          - log:
+                              message: "Order for 
${variable.customerData[name]}"
+                """);
+        
assertThat(result.getString("status")).as(String.valueOf(result)).isEqualTo("invalid");
+        assertThat(result.get("errors").toString()).contains("holds the file 
as it was read")
+                .doesNotContain("every item of a list");
+        assertThat(result.get("repairable").toString()).contains("the list 
item moved to the column of the list");
+    }
+
+    @Test
+    void anEditKeepsWhatTheWriteRepairsByItself(@TempDir Path dir) throws 
IOException {
+        Files.writeString(dir.resolve(FILE), """
+                - route:
+                    from:
+                      uri: file:orders
+                      steps:
+                        - unmarshal:
+                            json: {}
+                        - pollEnrich:
+                            expression:
+                              simple:
+                                expression: 
"file:customers?fileName=${body[customer]}.json&noop=true"
+                            timeout: 5000
+                            variableReceive: customerData
+                """);
+        JsonObject result = AuthoringTools.editFile(new ToolContext(), dir, 
FILE, """
+                            variableReceive: customerData
+                """, """
+                            variableReceive: customerData
+                          - log:
+                              message: "Order for 
${variable.customerData[name]}"
+                """);
+        
assertThat(result.getString("status")).as(String.valueOf(result)).isEqualTo("invalid");
+        assertThat(result.get("errors").toString()).contains("holds the file 
as it was read");
+        assertThat(result.getString("message")).startsWith("The file was not 
changed")
+                .contains("the write repairs it by itself");
+    }
+
+    @Test
+    void aProblemTheWriteCannotRepairKeepsTheAnswerAsItWas(@TempDir Path dir) {
+        // the case of library: is repaired, the misspelt key is not: both are 
said as written, nothing is hidden
+        JsonObject result = write(dir, """
+                - route:
+                    from:
+                      uri: timer:x
+                      steps:
+                        - unmarshal:
+                            json:
+                              library: jackson
+                        - log:
+                            mesage: "x"
+                """);
+        assertThat(result.getString("status")).isEqualTo("invalid");
+        assertThat(result.get("repairable")).isNull();
+        assertThat(result.get("errors").toString()).contains("mesage");
     }
 
     @Test

Reply via email to