Skip to content

Commit 1c70d13

Browse files
dmealingclaude
andauthored
fix(python): output-prompt emitter — nested field.object crashed gen/verify (#144)
The nested-object branch appended an inline `# FR-010: nested prompt deferred` comment to the emitted PromptField literal. spec_literal joins every field onto ONE line, so the Python '#' swallowed the rest of the line — including the closing '])' — emitting invalid Python (unterminated list, 'SyntaxError: [ was never closed'), which hard-crashed `gen` + `verify --codegen` (not just a diff) for any template.output whose payload has a nested field.object. The note now lives in the function source; the emitted literal carries no comment. (Python-only: TS/Java/Kotlin/C# use inline-safe /* */ block comments.) PromptField already defaults nested=None, so the placeholder is both valid syntax + constructible. Regression test fails pre-fix (SyntaxError), passes post-fix; FR-010 suite green. Claude-Session: https://claude.ai/code/session_01GF9xLEQZaPus5Y6opk398n Co-authored-by: Doug Mealing <noreply@anthropic.com>
1 parent 5741b16 commit 1c70d13

2 files changed

Lines changed: 67 additions & 4 deletions

File tree

server/python/src/metaobjects/codegen/output_format_spec_emitter.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,14 @@ def _prompt_field_literal(field: MetaData) -> str:
4444
array = "True" if fm.is_array(field) else "False"
4545

4646
if field.sub_type == fc.FIELD_SUBTYPE_OBJECT:
47-
return (
48-
f'PromptField("{name}", FieldKind.OBJECT, {req}, array={array})'
49-
" # FR-010: nested prompt deferred"
50-
)
47+
# FR-010: nested prompt deferred — emit a FieldKind.OBJECT placeholder (nested
48+
# defaults to None on PromptField). The note lives HERE, not in the returned
49+
# literal: spec_literal joins every field onto ONE line, and a Python '#' inline
50+
# comment would swallow the rest of that line — including the closing `])` — so a
51+
# nested-object payload emitted invalid Python (unterminated list) and crashed
52+
# `gen` + `verify --codegen`. The other ports use `/* */` block comments, which
53+
# are inline-safe; Python has none, so the literal must carry no comment.
54+
return f'PromptField("{name}", FieldKind.OBJECT, {req}, array={array})'
5155

5256
example = _opt_string_attr(field, fc.FIELD_ATTR_EXAMPLE)
5357
instruction = _opt_string_attr(field, fc.FIELD_ATTR_INSTRUCTION)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""Regression: a ``template.output`` payload with a NESTED ``field.object`` must emit
2+
VALID Python from the output-prompt spec emitter (a ``FieldKind.OBJECT`` placeholder),
3+
NOT crash ``gen`` / ``verify --codegen``.
4+
5+
The bug (0.15.0): the nested-object branch appended an inline ``# FR-010: nested prompt
6+
deferred`` comment to the emitted literal. ``spec_literal`` joins every field onto ONE
7+
line, so the Python ``#`` swallowed the rest of the line — including the closing ``])`` —
8+
producing an unterminated list (``SyntaxError: '[' was never closed``). The other four
9+
ports use ``/* */`` block comments, which are inline-safe; Python has none, so the literal
10+
must carry no comment.
11+
"""
12+
from __future__ import annotations
13+
14+
import ast
15+
16+
import metaobjects.core_types # noqa: F401 — side-effect: registers attr classes
17+
from metaobjects.codegen import output_format_spec_emitter as ofs
18+
from metaobjects.meta.core.field import field_constants as fc
19+
from metaobjects.meta.core.field.meta_field import MetaField
20+
from metaobjects.meta.core.object.meta_object import MetaObject
21+
from metaobjects.meta.template import template_constants as tc
22+
from metaobjects.meta.template.meta_template import MetaTemplate
23+
from metaobjects.shared.base_types import TYPE_FIELD, TYPE_OBJECT, TYPE_TEMPLATE
24+
25+
26+
def _envelope() -> tuple[MetaObject, MetaTemplate]:
27+
before = MetaField(TYPE_FIELD, fc.FIELD_SUBTYPE_STRING, "before")
28+
before.set_attr(fc.FIELD_ATTR_REQUIRED, True)
29+
# the nested object sits BETWEEN two scalars, so a swallowed line would eat both the
30+
# trailing field and the closing bracket.
31+
inner = MetaField(TYPE_FIELD, fc.FIELD_SUBTYPE_OBJECT, "inner")
32+
inner.set_attr(fc.FIELD_ATTR_REQUIRED, True)
33+
after = MetaField(TYPE_FIELD, fc.FIELD_SUBTYPE_STRING, "after")
34+
35+
vo = MetaObject(TYPE_OBJECT, "value", "Envelope")
36+
vo.package = "acme::ai"
37+
for f in (before, inner, after):
38+
vo.add_child(f)
39+
40+
tmpl = MetaTemplate(TYPE_TEMPLATE, tc.TEMPLATE_SUBTYPE_OUTPUT, "EnvOutput")
41+
tmpl.set_attr(tc.TEMPLATE_ATTR_PAYLOAD_REF, "Envelope")
42+
tmpl.set_attr(tc.TEMPLATE_ATTR_FORMAT, "xml")
43+
tmpl.set_attr(tc.TEMPLATE_ATTR_PROMPT_STYLE, "guide")
44+
return vo, tmpl
45+
46+
47+
def test_nested_object_payload_emits_parseable_python() -> None:
48+
vo, tmpl = _envelope()
49+
lit = ofs.spec_literal(vo, tmpl, "Envelope")
50+
51+
# 1) the whole OutputFormatSpec(...) literal must be valid Python — the core bug.
52+
ast.parse(lit) # raised "SyntaxError: '[' was never closed" on the broken output
53+
54+
# 2) the nested field is a FieldKind.OBJECT placeholder …
55+
assert "FieldKind.OBJECT" in lit
56+
# 3) … with NO inline comment in the emitted literal (that was the swallow) …
57+
assert "#" not in lit
58+
# 4) … and the field AFTER the nested one survived (was not commented out).
59+
assert '"after"' in lit

0 commit comments

Comments
 (0)