Skip to content

Commit 0bb4271

Browse files
dmealingclaude
andcommitted
fix(render): FR-010 renderer — non-finite numerics stay valid JSON; close GUIDE+JSON + escaping test gaps
- C1: isNumericOrBoolean now rejects NaN/Infinity so those values are quoted (valid JSON) instead of emitted bare (invalid JSON) - I1: remove assertFalse(out.contains("//")) from ExampleOnly + Inline tests — // inside a quoted string value is data, not a comment - Add nanDoubleStaysValidJsonQuoted, urlValueDoesNotBreakJson, jsonValueWithQuotesStaysValid to ExampleOnlyTest - Add guideJsonSkeletonIsValidJson to GuideTest (extracts skeleton after "Respond exactly like this:" to avoid prose braces) - M1: precondition javadoc on OutputFormatSpec and PromptField noting rootName/name must be identifier-safe - M2: inline comment in renderJsonSkeleton noting OBJECT/nested expansion is a Plan 3.1 deferral Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 7082c40 commit 0bb4271

6 files changed

Lines changed: 53 additions & 6 deletions

File tree

server/java/render/src/main/java/com/metaobjects/render/prompt/OutputFormatRenderer.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ private static String renderJsonSkeleton(OutputFormatSpec spec, PromptOverrides
140140
List<PromptField> fields = spec.fields();
141141
for (int i = 0; i < fields.size(); i++) {
142142
PromptField field = fields.get(i);
143+
// NOTE: FieldKind.OBJECT / nested fields are not expanded here — they render as a
144+
// "{fieldName}" placeholder. Nested-object expansion is a Plan 3.1 deferral.
143145
String value = exampleValue(field, overrides);
144146
boolean isLast = i == fields.size() - 1;
145147
sb.append(" \"").append(field.name()).append("\": ");
@@ -178,10 +180,11 @@ private static boolean isNumericOrBoolean(FieldKind kind, String value) {
178180
if (!NUMERIC_KINDS.contains(kind)) return false;
179181
if (value.equals("true") || value.equals("false")) return true;
180182
try {
181-
Double.parseDouble(value);
182-
return true;
183+
double d = Double.parseDouble(value);
184+
return !Double.isNaN(d) && !Double.isInfinite(d);
183185
} catch (NumberFormatException e) {
184-
return false;
186+
// not a number; fall through to quoted string
185187
}
188+
return false;
186189
}
187190
}

server/java/render/src/main/java/com/metaobjects/render/prompt/OutputFormatSpec.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
import java.util.List;
55
import java.util.Objects;
66

7+
/**
8+
* Precondition: {@code rootName} must be identifier-safe (valid XML element name / JSON key).
9+
* The renderer does not escape it.
10+
*/
711
public record OutputFormatSpec(Format format, String rootName, PromptStyle style, List<PromptField> fields) {
812
public OutputFormatSpec {
913
Objects.requireNonNull(format, "format");

server/java/render/src/main/java/com/metaobjects/render/prompt/PromptField.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
import java.util.List;
55
import java.util.Map;
66

7-
/** enumValues/enumDoc non-null only for ENUM; nested non-null only for OBJECT; example/instruction nullable. */
7+
/**
8+
* enumValues/enumDoc non-null only for ENUM; nested non-null only for OBJECT; example/instruction nullable.
9+
* Precondition: {@code name} must be identifier-safe (valid XML element name / JSON key).
10+
* The renderer does not escape field names.
11+
*/
812
public record PromptField(String name, FieldKind kind, boolean required, boolean array,
913
List<String> enumValues, Map<String, String> enumDoc,
1014
String example, String instruction, OutputFormatSpec nested) {}

server/java/render/src/test/java/com/metaobjects/render/prompt/OutputFormatRendererExampleOnlyTest.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ private OutputFormatSpec spec(Format fmt) {
2828

2929
@Test public void jsonExampleBlockIsValidJson() throws Exception {
3030
String out = OutputFormatRenderer.render(spec(Format.JSON), PromptOverrides.none());
31-
assertFalse("no // comments", out.contains("//"));
3231
int open = out.indexOf('{'), close = out.lastIndexOf('}');
3332
String json = out.substring(open, close + 1);
3433
var node = JSON.readTree(json);
@@ -41,4 +40,28 @@ private OutputFormatSpec spec(Format fmt) {
4140
String out = OutputFormatRenderer.render(spec(Format.XML), ov);
4241
assertTrue(out.contains("<text>OVERRIDDEN</text>"));
4342
}
43+
44+
@Test public void nanDoubleStaysValidJsonQuoted() throws Exception {
45+
OutputFormatSpec s = new OutputFormatSpec(Format.JSON, "Answer", PromptStyle.EXAMPLE_ONLY, List.of(
46+
new PromptField("score", FieldKind.DOUBLE, true, false, null, null, "NaN", null, null)));
47+
String out = OutputFormatRenderer.render(s, PromptOverrides.none());
48+
String json = out.substring(out.indexOf('{'), out.lastIndexOf('}') + 1);
49+
assertEquals("NaN", JSON.readTree(json).get("score").asText()); // quoted string, valid JSON
50+
}
51+
52+
@Test public void urlValueDoesNotBreakJson() throws Exception {
53+
OutputFormatSpec s = new OutputFormatSpec(Format.JSON, "Answer", PromptStyle.EXAMPLE_ONLY, List.of(
54+
new PromptField("link", FieldKind.STRING, true, false, null, null, "https://example.com/x", null, null)));
55+
String out = OutputFormatRenderer.render(s, PromptOverrides.none());
56+
String json = out.substring(out.indexOf('{'), out.lastIndexOf('}') + 1);
57+
assertEquals("https://example.com/x", JSON.readTree(json).get("link").asText());
58+
}
59+
60+
@Test public void jsonValueWithQuotesStaysValid() throws Exception {
61+
OutputFormatSpec s = new OutputFormatSpec(Format.JSON, "Answer", PromptStyle.EXAMPLE_ONLY, List.of(
62+
new PromptField("q", FieldKind.STRING, true, false, null, null, "she said \"hi\"", null, null)));
63+
String out = OutputFormatRenderer.render(s, PromptOverrides.none());
64+
String json = out.substring(out.indexOf('{'), out.lastIndexOf('}') + 1);
65+
assertEquals("she said \"hi\"", JSON.readTree(json).get("q").asText());
66+
}
4467
}

server/java/render/src/test/java/com/metaobjects/render/prompt/OutputFormatRendererGuideTest.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,18 @@ private OutputFormatSpec spec() {
3636
PromptOverrides ov = new PromptOverrides(null, Map.of(), Map.of("text", "NEW INSTRUCTION"));
3737
assertTrue(OutputFormatRenderer.render(spec(), ov).contains("NEW INSTRUCTION"));
3838
}
39+
40+
@Test public void guideJsonSkeletonIsValidJson() throws Exception {
41+
com.fasterxml.jackson.databind.ObjectMapper m = new com.fasterxml.jackson.databind.ObjectMapper();
42+
OutputFormatSpec s = new OutputFormatSpec(Format.JSON, "Answer", PromptStyle.GUIDE, List.of(
43+
new PromptField("text", FieldKind.STRING, true, false, null, null, "hi {now}", "say {x}", null),
44+
new PromptField("confidence", FieldKind.ENUM, true, false, List.of("HIGH","LOW"),
45+
Map.of("HIGH","Directly supported."), "HIGH", null, null)));
46+
String out = OutputFormatRenderer.render(s, PromptOverrides.none());
47+
String afterMarker = out.substring(out.indexOf("Respond exactly like this:"));
48+
String json = afterMarker.substring(afterMarker.indexOf('{'), afterMarker.lastIndexOf('}') + 1);
49+
var node = m.readTree(json); // must parse even though prose above contains { } braces
50+
assertEquals("hi {now}", node.get("text").asText());
51+
assertEquals("HIGH", node.get("confidence").asText());
52+
}
3953
}

server/java/render/src/test/java/com/metaobjects/render/prompt/OutputFormatRendererInlineTest.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ private OutputFormatSpec spec(Format f) {
2323
}
2424
@Test public void jsonInlineStillValid() throws Exception {
2525
String out = OutputFormatRenderer.render(spec(Format.JSON), PromptOverrides.none());
26-
assertFalse(out.contains("//"));
2726
String json = out.substring(out.indexOf('{'), out.lastIndexOf('}') + 1);
2827
assertEquals("HIGH | OK | LOW", JSON.readTree(json).get("confidence").asText());
2928
}

0 commit comments

Comments
 (0)