metaobjects 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. metaobjects/__init__.py +75 -0
  2. metaobjects/agent_context/__init__.py +55 -0
  3. metaobjects/agent_context/_content/README.md +14 -0
  4. metaobjects/agent_context/_content/servers/csharp.meta.json +5 -0
  5. metaobjects/agent_context/_content/servers/java.meta.json +5 -0
  6. metaobjects/agent_context/_content/servers/kotlin.meta.json +5 -0
  7. metaobjects/agent_context/_content/servers/python.meta.json +5 -0
  8. metaobjects/agent_context/_content/servers/typescript.meta.json +5 -0
  9. metaobjects/agent_context/_content/skills/metaobjects-authoring/SKILL.md +301 -0
  10. metaobjects/agent_context/_content/skills/metaobjects-codegen/SKILL.md +99 -0
  11. metaobjects/agent_context/_content/skills/metaobjects-codegen/references/csharp.md +87 -0
  12. metaobjects/agent_context/_content/skills/metaobjects-codegen/references/java.md +94 -0
  13. metaobjects/agent_context/_content/skills/metaobjects-codegen/references/kotlin.md +110 -0
  14. metaobjects/agent_context/_content/skills/metaobjects-codegen/references/typescript.md +135 -0
  15. metaobjects/agent_context/_content/skills/metaobjects-prompts/SKILL.md +148 -0
  16. metaobjects/agent_context/_content/skills/metaobjects-prompts/references/csharp.md +110 -0
  17. metaobjects/agent_context/_content/skills/metaobjects-prompts/references/java.md +108 -0
  18. metaobjects/agent_context/_content/skills/metaobjects-prompts/references/kotlin.md +130 -0
  19. metaobjects/agent_context/_content/skills/metaobjects-prompts/references/python.md +116 -0
  20. metaobjects/agent_context/_content/skills/metaobjects-prompts/references/typescript.md +150 -0
  21. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/SKILL.md +130 -0
  22. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/java.md +96 -0
  23. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/kotlin.md +99 -0
  24. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/react.md +86 -0
  25. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/tanstack.md +119 -0
  26. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/typescript.md +92 -0
  27. metaobjects/agent_context/_content/skills/metaobjects-verify/SKILL.md +107 -0
  28. metaobjects/agent_context/_content/skills/metaobjects-verify/references/migration.md +72 -0
  29. metaobjects/agent_context/_content/templates/always-on.md.mustache +27 -0
  30. metaobjects/agent_context/assemble.py +133 -0
  31. metaobjects/agent_context/content_root.py +54 -0
  32. metaobjects/agent_context/scaffold.py +191 -0
  33. metaobjects/agent_context/types.py +44 -0
  34. metaobjects/attr_class_map.py +23 -0
  35. metaobjects/cli.py +696 -0
  36. metaobjects/codegen/__init__.py +0 -0
  37. metaobjects/codegen/config.py +11 -0
  38. metaobjects/codegen/constants.py +13 -0
  39. metaobjects/codegen/extract_delegate_emitter.py +384 -0
  40. metaobjects/codegen/extract_schema_emitter.py +139 -0
  41. metaobjects/codegen/format.py +31 -0
  42. metaobjects/codegen/fr010_field_mapping.py +220 -0
  43. metaobjects/codegen/generator.py +62 -0
  44. metaobjects/codegen/generator_registry.py +163 -0
  45. metaobjects/codegen/generators/__init__.py +0 -0
  46. metaobjects/codegen/generators/entity_model.py +263 -0
  47. metaobjects/codegen/generators/extractor_generator.py +317 -0
  48. metaobjects/codegen/generators/filter_allowlist_generator.py +309 -0
  49. metaobjects/codegen/generators/m2m_codegen.py +192 -0
  50. metaobjects/codegen/generators/output_parser_generator.py +272 -0
  51. metaobjects/codegen/generators/output_prompt_generator.py +192 -0
  52. metaobjects/codegen/generators/payload_vo_generator.py +672 -0
  53. metaobjects/codegen/generators/render_helper_generator.py +451 -0
  54. metaobjects/codegen/generators/router_generator.py +635 -0
  55. metaobjects/codegen/generators/template_generator.py +70 -0
  56. metaobjects/codegen/generators/tph_plan.py +120 -0
  57. metaobjects/codegen/generators/trace_helper_generator.py +336 -0
  58. metaobjects/codegen/instance_artifacts.py +15 -0
  59. metaobjects/codegen/output_format_spec_emitter.py +79 -0
  60. metaobjects/codegen/overwrite_policy.py +27 -0
  61. metaobjects/codegen/runner.py +110 -0
  62. metaobjects/codegen/runtime/__init__.py +6 -0
  63. metaobjects/codegen/runtime/filter_parser.py +193 -0
  64. metaobjects/codegen/type_map.py +84 -0
  65. metaobjects/core_types.py +809 -0
  66. metaobjects/datatype.py +19 -0
  67. metaobjects/documentation/__init__.py +28 -0
  68. metaobjects/documentation/doc_constants.py +20 -0
  69. metaobjects/documentation/doc_provider.py +20 -0
  70. metaobjects/documentation/doc_schema.py +24 -0
  71. metaobjects/errors.py +124 -0
  72. metaobjects/loader/__init__.py +0 -0
  73. metaobjects/loader/merge.py +287 -0
  74. metaobjects/loader/meta_data_loader.py +245 -0
  75. metaobjects/loader/sources/__init__.py +24 -0
  76. metaobjects/loader/sources/directory_source.py +50 -0
  77. metaobjects/loader/sources/file_source.py +41 -0
  78. metaobjects/loader/sources/meta_data_source.py +67 -0
  79. metaobjects/loader/sources/uri_source.py +56 -0
  80. metaobjects/loader/validate_discriminator.py +181 -0
  81. metaobjects/loader/validate_field_readonly.py +146 -0
  82. metaobjects/loader/validate_source_parameter_ref.py +159 -0
  83. metaobjects/loader/validate_source_physical_names.py +140 -0
  84. metaobjects/loader/validation_passes.py +1513 -0
  85. metaobjects/meta/__init__.py +1 -0
  86. metaobjects/meta/core/__init__.py +0 -0
  87. metaobjects/meta/core/attr/__init__.py +0 -0
  88. metaobjects/meta/core/attr/attr_constants.py +31 -0
  89. metaobjects/meta/core/attr/meta_attr.py +136 -0
  90. metaobjects/meta/core/field/__init__.py +0 -0
  91. metaobjects/meta/core/field/field_constants.py +105 -0
  92. metaobjects/meta/core/field/meta_field.py +76 -0
  93. metaobjects/meta/core/identity/__init__.py +0 -0
  94. metaobjects/meta/core/identity/identity_constants.py +19 -0
  95. metaobjects/meta/core/identity/meta_identity.py +8 -0
  96. metaobjects/meta/core/object/__init__.py +0 -0
  97. metaobjects/meta/core/object/meta_object.py +65 -0
  98. metaobjects/meta/core/object/meta_object_aware.py +43 -0
  99. metaobjects/meta/core/object/object_class_registry.py +56 -0
  100. metaobjects/meta/core/object/object_constants.py +13 -0
  101. metaobjects/meta/core/object/object_extract.py +400 -0
  102. metaobjects/meta/core/object/value_object.py +70 -0
  103. metaobjects/meta/core/relationship/__init__.py +0 -0
  104. metaobjects/meta/core/relationship/derive_m2m_fields.py +180 -0
  105. metaobjects/meta/core/relationship/meta_relationship.py +54 -0
  106. metaobjects/meta/core/relationship/relationship_constants.py +51 -0
  107. metaobjects/meta/core/validator/__init__.py +0 -0
  108. metaobjects/meta/core/validator/validator_constants.py +18 -0
  109. metaobjects/meta/meta_data.py +206 -0
  110. metaobjects/meta/meta_root.py +8 -0
  111. metaobjects/meta/persistence/__init__.py +0 -0
  112. metaobjects/meta/persistence/db/__init__.py +1 -0
  113. metaobjects/meta/persistence/db/db_constants.py +41 -0
  114. metaobjects/meta/persistence/db/db_provider.py +60 -0
  115. metaobjects/meta/persistence/origin/__init__.py +0 -0
  116. metaobjects/meta/persistence/origin/meta_origin.py +8 -0
  117. metaobjects/meta/persistence/origin/origin_constants.py +20 -0
  118. metaobjects/meta/persistence/source/__init__.py +0 -0
  119. metaobjects/meta/persistence/source/meta_source.py +137 -0
  120. metaobjects/meta/persistence/source/source_constants.py +115 -0
  121. metaobjects/meta/presentation/__init__.py +0 -0
  122. metaobjects/meta/presentation/layout/__init__.py +0 -0
  123. metaobjects/meta/presentation/layout/layout_constants.py +13 -0
  124. metaobjects/meta/presentation/layout/meta_layout.py +8 -0
  125. metaobjects/meta/presentation/view/__init__.py +0 -0
  126. metaobjects/meta/presentation/view/meta_view.py +8 -0
  127. metaobjects/meta/presentation/view/view_constants.py +22 -0
  128. metaobjects/meta/template/__init__.py +0 -0
  129. metaobjects/meta/template/meta_template.py +46 -0
  130. metaobjects/meta/template/template_constants.py +112 -0
  131. metaobjects/meta/template/template_provider.py +43 -0
  132. metaobjects/parser.py +380 -0
  133. metaobjects/parser_yaml.py +82 -0
  134. metaobjects/provider.py +111 -0
  135. metaobjects/py.typed +0 -0
  136. metaobjects/registry.py +210 -0
  137. metaobjects/registry_manifest.py +223 -0
  138. metaobjects/render/__init__.py +74 -0
  139. metaobjects/render/email_document.py +14 -0
  140. metaobjects/render/escapers.py +109 -0
  141. metaobjects/render/extract/__init__.py +59 -0
  142. metaobjects/render/extract/coerce.py +279 -0
  143. metaobjects/render/extract/extract.py +211 -0
  144. metaobjects/render/extract/extract_map.py +61 -0
  145. metaobjects/render/extract/json_forgiving_reader.py +203 -0
  146. metaobjects/render/extract/locate.py +65 -0
  147. metaobjects/render/extract/normalize.py +96 -0
  148. metaobjects/render/extract/strip.py +20 -0
  149. metaobjects/render/extract/types.py +332 -0
  150. metaobjects/render/extract/xml_forgiving_reader.py +162 -0
  151. metaobjects/render/filesystem_provider.py +51 -0
  152. metaobjects/render/prompt/__init__.py +32 -0
  153. metaobjects/render/prompt/output_format_renderer.py +340 -0
  154. metaobjects/render/prompt/output_format_spec.py +28 -0
  155. metaobjects/render/prompt/prompt_field.py +29 -0
  156. metaobjects/render/prompt/prompt_overrides.py +29 -0
  157. metaobjects/render/prompt/prompt_style.py +38 -0
  158. metaobjects/render/renderer.py +358 -0
  159. metaobjects/render/verify.py +266 -0
  160. metaobjects/runtime/__init__.py +39 -0
  161. metaobjects/runtime/llm_recorder.py +210 -0
  162. metaobjects/runtime/n2m_resolver.py +155 -0
  163. metaobjects/runtime/object_manager.py +715 -0
  164. metaobjects/runtime/tph.py +50 -0
  165. metaobjects/serializer_json.py +172 -0
  166. metaobjects/shared/__init__.py +0 -0
  167. metaobjects/shared/base_types.py +16 -0
  168. metaobjects/shared/separators.py +4 -0
  169. metaobjects/shared/structural.py +9 -0
  170. metaobjects/source/__init__.py +79 -0
  171. metaobjects/source/error_source.py +266 -0
  172. metaobjects/source/json_path.py +106 -0
  173. metaobjects/source/semantic_diff.py +98 -0
  174. metaobjects/source/yaml_positions.py +174 -0
  175. metaobjects/super_resolve.py +128 -0
  176. metaobjects/yaml_desugar.py +481 -0
  177. metaobjects-0.9.0.dist-info/METADATA +97 -0
  178. metaobjects-0.9.0.dist-info/RECORD +181 -0
  179. metaobjects-0.9.0.dist-info/WHEEL +4 -0
  180. metaobjects-0.9.0.dist-info/entry_points.txt +2 -0
  181. metaobjects-0.9.0.dist-info/licenses/LICENSE +189 -0
@@ -0,0 +1,340 @@
1
+ """FR-010 artifact 1 — output-format prompt renderer ("produce your answer like this").
2
+
3
+ Renders an :class:`OutputFormatSpec` into a prompt fragment that teaches an LLM how
4
+ to shape its answer. Three comment-free styles (guide / inline / exampleOnly) × two
5
+ formats (json / xml). Guidance is carried in prose / inline placeholders / a filled
6
+ skeleton — NEVER in comments (models ignore them).
7
+
8
+ Cross-port INVARIANT: the rendered text is byte-identical to the Java/C#/Kotlin/TS
9
+ reference (``com.metaobjects.render.prompt.OutputFormatRenderer``). Do not change the
10
+ verbatim prose, skeleton shapes, or the numeric-vs-quoted decision.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import re
15
+
16
+ import metaobjects.render.escapers as escapers
17
+ from metaobjects.render.prompt.output_format_spec import OutputFormatSpec
18
+ from metaobjects.render.prompt.prompt_field import PromptField
19
+ from metaobjects.render.prompt.prompt_overrides import PromptOverrides
20
+ from metaobjects.render.prompt.prompt_style import PromptStyle
21
+ from metaobjects.render.extract import FieldKind, Format
22
+
23
+ _NUMERIC_KINDS: frozenset[FieldKind] = frozenset(
24
+ {FieldKind.INT, FieldKind.LONG, FieldKind.DOUBLE, FieldKind.BOOLEAN}
25
+ )
26
+
27
+ _INDENT = " "
28
+ _MAX_NEST_DEPTH = 8
29
+
30
+ # Render mode shared by the example/inline skeleton recursion.
31
+ _MODE_EXAMPLE = "example"
32
+ _MODE_INLINE = "inline"
33
+
34
+ # The render engine OWNS format-keyed escaping; the Format enum's UPPER values
35
+ # ("JSON"/"XML") map to the lowercase escaper-registry keys.
36
+ def _escape_xml(s: str) -> str:
37
+ return escapers.escape(escapers.FORMAT_XML, s)
38
+
39
+
40
+ def _escape_json(s: str) -> str:
41
+ return escapers.escape(escapers.FORMAT_JSON, s)
42
+
43
+
44
+ def render_output_format(spec: OutputFormatSpec, overrides: PromptOverrides) -> str:
45
+ """Render an :class:`OutputFormatSpec` into an output-format prompt fragment.
46
+
47
+ The effective style is the override's style if present, otherwise the spec's.
48
+ """
49
+ effective_style = overrides.style if overrides.style is not None else spec.style
50
+ if effective_style is PromptStyle.EXAMPLE_ONLY:
51
+ return _render_example_only(spec, overrides)
52
+ if effective_style is PromptStyle.INLINE:
53
+ return _render_inline(spec, overrides)
54
+ return _render_guide(spec, overrides)
55
+
56
+
57
+ # ---- INLINE ----------------------------------------------------------------
58
+
59
+
60
+ def _render_inline(spec: OutputFormatSpec, overrides: PromptOverrides) -> str:
61
+ if spec.format is Format.XML:
62
+ return _render_xml_skeleton(spec, overrides, _MODE_INLINE)
63
+ return _render_json_skeleton(spec, overrides, _MODE_INLINE)
64
+
65
+
66
+ def _inline_content(field: PromptField, overrides: PromptOverrides) -> str:
67
+ if field.kind is FieldKind.ENUM and field.enum_values:
68
+ return " | ".join(field.enum_values)
69
+ if field.kind is FieldKind.BOOLEAN:
70
+ return "true | false"
71
+ instruction = _resolve_instruction(field, overrides)
72
+ if instruction is not None:
73
+ return "{" + instruction + "}"
74
+ return "{" + field.name + "}"
75
+
76
+
77
+ def _resolve_instruction(field: PromptField, overrides: PromptOverrides) -> str | None:
78
+ """Effective instruction: override first, then the field default, else None."""
79
+ ov = overrides.instructions.get(field.name)
80
+ if ov is not None:
81
+ return ov
82
+ return field.instruction
83
+
84
+
85
+ # ---- GUIDE -----------------------------------------------------------------
86
+
87
+
88
+ def _render_guide(spec: OutputFormatSpec, overrides: PromptOverrides) -> str:
89
+ sb = "Fill in each field as described below:\n"
90
+ sb += _guide_fields(spec, overrides, "", {id(spec)}, 0)
91
+ sb += "\nRespond exactly like this:\n"
92
+ sb += _render_example_only(spec, overrides)
93
+ return sb
94
+
95
+
96
+ def _guide_fields(
97
+ spec: OutputFormatSpec,
98
+ overrides: PromptOverrides,
99
+ prefix: str,
100
+ path: set[int],
101
+ depth: int,
102
+ ) -> str:
103
+ sb = ""
104
+ for field in spec.fields:
105
+ display_name = prefix + field.name
106
+ sb += _guide_entry(field, overrides, display_name)
107
+ if _can_expand(field, path, depth):
108
+ nested = field.nested
109
+ assert nested is not None
110
+ child_prefix = (
111
+ f"{display_name}[]." if field.array else f"{display_name}."
112
+ )
113
+ path.add(id(nested))
114
+ sb += _guide_fields(nested, overrides, child_prefix, path, depth + 1)
115
+ path.discard(id(nested))
116
+ return sb
117
+
118
+
119
+ def _guide_entry(
120
+ field: PromptField, overrides: PromptOverrides, display_name: str
121
+ ) -> str:
122
+ req = "required" if field.required else "optional"
123
+ sb = f"- {display_name} ({req})"
124
+ instruction = _resolve_instruction(field, overrides)
125
+ if instruction is not None:
126
+ sb += f": {instruction}"
127
+ sb += "\n"
128
+ if field.kind is FieldKind.ENUM and field.enum_values:
129
+ sb += f" one of {', '.join(field.enum_values)}\n"
130
+ enum_doc = field.enum_doc
131
+ if enum_doc is not None:
132
+ for val in field.enum_values:
133
+ doc = enum_doc.get(val)
134
+ if doc is not None:
135
+ sb += f" {val} = {doc}\n"
136
+ eg = _example_value_if_declared(field, overrides)
137
+ if eg is not None:
138
+ sb += f" e.g. {eg}\n"
139
+ return sb
140
+
141
+
142
+ # ---- EXAMPLE-ONLY (also the skeleton appended by GUIDE) ---------------------
143
+
144
+
145
+ def _render_example_only(spec: OutputFormatSpec, overrides: PromptOverrides) -> str:
146
+ if spec.format is Format.XML:
147
+ return _render_xml_skeleton(spec, overrides, _MODE_EXAMPLE)
148
+ return _render_json_skeleton(spec, overrides, _MODE_EXAMPLE)
149
+
150
+
151
+ # ---- JSON skeleton (recursive) ---------------------------------------------
152
+
153
+
154
+ def _render_json_skeleton(
155
+ spec: OutputFormatSpec, overrides: PromptOverrides, mode: str
156
+ ) -> str:
157
+ return _json_object(spec, overrides, "", mode, {id(spec)}, 0)
158
+
159
+
160
+ def _json_object(
161
+ spec: OutputFormatSpec,
162
+ overrides: PromptOverrides,
163
+ brace_indent: str,
164
+ mode: str,
165
+ path: set[int],
166
+ depth: int,
167
+ ) -> str:
168
+ # Empty object is `{\n<brace_indent>}` (cross-port parity), not `{\n\n}`.
169
+ if not spec.fields:
170
+ return f"{{\n{brace_indent}}}"
171
+ field_indent = brace_indent + _INDENT
172
+ lines = [
173
+ f'{field_indent}"{field.name}": '
174
+ f"{_json_value(field, overrides, field_indent, mode, path, depth)}"
175
+ for field in spec.fields
176
+ ]
177
+ return "{\n" + ",\n".join(lines) + f"\n{brace_indent}}}"
178
+
179
+
180
+ def _json_value(
181
+ field: PromptField,
182
+ overrides: PromptOverrides,
183
+ indent: str,
184
+ mode: str,
185
+ path: set[int],
186
+ depth: int,
187
+ ) -> str:
188
+ if field.array:
189
+ return _json_array(field, overrides, indent, mode, path, depth)
190
+ if field.kind is FieldKind.OBJECT:
191
+ return _json_object_field(field, overrides, indent, mode, path, depth)
192
+ return _json_leaf(field, overrides, mode)
193
+
194
+
195
+ def _json_leaf(field: PromptField, overrides: PromptOverrides, mode: str) -> str:
196
+ if mode == _MODE_INLINE:
197
+ return '"' + _escape_json(_inline_content(field, overrides)) + '"'
198
+ value = _example_value(field, overrides)
199
+ if _is_numeric_or_boolean(field.kind, value):
200
+ return value
201
+ return '"' + _escape_json(value) + '"'
202
+
203
+
204
+ def _json_object_field(
205
+ field: PromptField,
206
+ overrides: PromptOverrides,
207
+ indent: str,
208
+ mode: str,
209
+ path: set[int],
210
+ depth: int,
211
+ ) -> str:
212
+ if not _can_expand(field, path, depth):
213
+ return _json_leaf(field, overrides, mode)
214
+ nested = field.nested
215
+ assert nested is not None
216
+ path.add(id(nested))
217
+ out = _json_object(nested, overrides, indent, mode, path, depth + 1)
218
+ path.discard(id(nested))
219
+ return out
220
+
221
+
222
+ def _json_array(
223
+ field: PromptField,
224
+ overrides: PromptOverrides,
225
+ indent: str,
226
+ mode: str,
227
+ path: set[int],
228
+ depth: int,
229
+ ) -> str:
230
+ elem_indent = indent + _INDENT
231
+ if _can_expand(field, path, depth):
232
+ nested = field.nested
233
+ assert nested is not None
234
+ path.add(id(nested))
235
+ elem = _json_object(nested, overrides, elem_indent, mode, path, depth + 1)
236
+ path.discard(id(nested))
237
+ else:
238
+ elem = _json_leaf(field, overrides, mode)
239
+ return f"[\n{elem_indent}{elem}\n{indent}]"
240
+
241
+
242
+ # ---- XML skeleton (recursive) ----------------------------------------------
243
+
244
+
245
+ def _render_xml_skeleton(
246
+ spec: OutputFormatSpec, overrides: PromptOverrides, mode: str
247
+ ) -> str:
248
+ body = _xml_body(spec, overrides, _INDENT, mode, {id(spec)}, 0)
249
+ return f"<{spec.root_name}>\n{body}</{spec.root_name}>"
250
+
251
+
252
+ def _xml_body(
253
+ spec: OutputFormatSpec,
254
+ overrides: PromptOverrides,
255
+ indent: str,
256
+ mode: str,
257
+ path: set[int],
258
+ depth: int,
259
+ ) -> str:
260
+ return "".join(
261
+ _xml_field(field, overrides, indent, mode, path, depth)
262
+ for field in spec.fields
263
+ )
264
+
265
+
266
+ def _xml_field(
267
+ field: PromptField,
268
+ overrides: PromptOverrides,
269
+ indent: str,
270
+ mode: str,
271
+ path: set[int],
272
+ depth: int,
273
+ ) -> str:
274
+ if _can_expand(field, path, depth):
275
+ nested = field.nested
276
+ assert nested is not None
277
+ path.add(id(nested))
278
+ body = _xml_body(nested, overrides, indent + _INDENT, mode, path, depth + 1)
279
+ path.discard(id(nested))
280
+ return f"{indent}<{field.name}>\n{body}{indent}</{field.name}>\n"
281
+ content = (
282
+ _inline_content(field, overrides)
283
+ if mode == _MODE_INLINE
284
+ else _example_value(field, overrides)
285
+ )
286
+ return f"{indent}<{field.name}>{_escape_xml(content)}</{field.name}>\n"
287
+
288
+
289
+ # ---- nested-expansion guard ------------------------------------------------
290
+
291
+
292
+ def _can_expand(field: PromptField, path: set[int], depth: int) -> bool:
293
+ """Expand an OBJECT field only when it has a nested spec, the depth bound is not
294
+ exceeded, and it would not re-enter a spec already on the current path.
295
+
296
+ The cycle guard is REFERENCE IDENTITY via ``id()``: frozen dataclasses compare by
297
+ value (``eq=True``), so two value-equal sibling specs must both still expand. ``id``
298
+ keys distinguish them.
299
+ """
300
+ return (
301
+ field.kind is FieldKind.OBJECT
302
+ and field.nested is not None
303
+ and depth < _MAX_NEST_DEPTH
304
+ and id(field.nested) not in path
305
+ )
306
+
307
+
308
+ def _example_value_if_declared(field: PromptField, overrides: PromptOverrides) -> str | None:
309
+ from_override = overrides.examples.get(field.name)
310
+ if from_override is not None:
311
+ return from_override
312
+ if field.example is not None:
313
+ return field.example
314
+ return None
315
+
316
+
317
+ def _example_value(field: PromptField, overrides: PromptOverrides) -> str:
318
+ from_override = overrides.examples.get(field.name)
319
+ if from_override is not None:
320
+ return from_override
321
+ if field.example is not None:
322
+ return field.example
323
+ if field.kind is FieldKind.ENUM and field.enum_values:
324
+ return field.enum_values[0]
325
+ return "{" + field.name + "}"
326
+
327
+
328
+ # A canonical ASCII numeric literal. Mirrors the extract engine's `_ASCII_NUMERIC`:
329
+ # `[0-9]` (not `\d`) keeps it ASCII-only, rejecting Unicode digits, underscore digit
330
+ # grouping, and radix prefixes (0x../0b../0o..) so the emitted JSON stays valid and
331
+ # cross-port-identical.
332
+ _ASCII_NUMERIC = re.compile(r"^[+-]?(?:[0-9]+\.?[0-9]*|\.[0-9]+)(?:[eE][+-]?[0-9]+)?$")
333
+
334
+
335
+ def _is_numeric_or_boolean(kind: FieldKind, value: str) -> bool:
336
+ if kind not in _NUMERIC_KINDS:
337
+ return False
338
+ if value in ("true", "false"):
339
+ return True
340
+ return bool(_ASCII_NUMERIC.match(value.strip()))
@@ -0,0 +1,28 @@
1
+ """FR-010 artifact 1 — a complete output-format descriptor."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass, field
5
+ from typing import TYPE_CHECKING
6
+
7
+ from metaobjects.render.extract import Format
8
+ from metaobjects.render.prompt.prompt_style import PromptStyle
9
+
10
+ if TYPE_CHECKING:
11
+ from metaobjects.render.prompt.prompt_field import PromptField
12
+
13
+
14
+ @dataclass(frozen=True, slots=True)
15
+ class OutputFormatSpec:
16
+ """A complete output-format descriptor.
17
+
18
+ The format, the root element/object name, the default presentation style, and the
19
+ ordered fields. Drives :func:`render_output_format`.
20
+
21
+ Precondition: ``root_name`` must be identifier-safe (valid XML element name / JSON
22
+ key). The renderer does not escape it.
23
+ """
24
+
25
+ format: Format
26
+ root_name: str
27
+ style: PromptStyle
28
+ fields: list[PromptField] = field(default_factory=list)
@@ -0,0 +1,29 @@
1
+ """FR-010 artifact 1 — one field of an output-format fragment."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+
6
+ from metaobjects.render.extract import FieldKind
7
+ from metaobjects.render.prompt.output_format_spec import OutputFormatSpec
8
+
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class PromptField:
12
+ """One field of an output-format fragment.
13
+
14
+ ``enum_values``/``enum_doc`` are non-None only for ENUM; ``nested`` non-None only
15
+ for OBJECT; ``example``/``instruction`` are nullable.
16
+
17
+ Precondition: ``name`` must be identifier-safe (valid XML element name / JSON key).
18
+ The renderer does not escape field names.
19
+ """
20
+
21
+ name: str
22
+ kind: FieldKind
23
+ required: bool
24
+ array: bool = False
25
+ enum_values: list[str] | None = None
26
+ enum_doc: dict[str, str] | None = None
27
+ example: str | None = None
28
+ instruction: str | None = None
29
+ nested: OutputFormatSpec | None = None
@@ -0,0 +1,29 @@
1
+ """FR-010 artifact 1 — render-time overrides of the metadata defaults."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass, field
5
+ from typing import Final
6
+
7
+ from metaobjects.render.prompt.prompt_style import PromptStyle
8
+
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class PromptOverrides:
12
+ """Render-time overrides of the metadata defaults.
13
+
14
+ ``style`` of ``None`` keeps the spec's style; the maps override
15
+ ``PromptField.example``/``PromptField.instruction`` per field name.
16
+ """
17
+
18
+ style: PromptStyle | None = None
19
+ examples: dict[str, str] = field(default_factory=dict)
20
+ instructions: dict[str, str] = field(default_factory=dict)
21
+
22
+
23
+ # No overrides — keep every metadata default. Mirrors Java ``PromptOverrides.none()``.
24
+ PROMPT_OVERRIDES_NONE: Final = PromptOverrides()
25
+
26
+
27
+ def no_overrides() -> PromptOverrides:
28
+ """No overrides — keep every metadata default."""
29
+ return PROMPT_OVERRIDES_NONE
@@ -0,0 +1,38 @@
1
+ """FR-010 artifact 1 — how the output-format fragment teaches the model.
2
+
3
+ Guidance is NEVER carried in comments (models ignore them) — it lives in prose /
4
+ inline placeholders / a filled skeleton. Default is ``"guide"``.
5
+
6
+ Tier-2 idiomatic Python: the Java SCREAMING_SNAKE enum (GUIDE/INLINE/EXAMPLE_ONLY)
7
+ becomes a string enum whose VALUES are the wire ``@promptStyle`` values
8
+ (``"guide"`` / ``"inline"`` / ``"exampleOnly"``) — no name<->wire mapping table.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from enum import Enum
13
+
14
+
15
+ class PromptStyle(Enum):
16
+ """Presentation style of the rendered output-format fragment.
17
+
18
+ - ``GUIDE``: prose field list ("Fill in each field…") followed by an example skeleton.
19
+ - ``INLINE``: a single skeleton whose field values are inline placeholders / enum choices.
20
+ - ``EXAMPLE_ONLY``: just a filled example skeleton, nothing else.
21
+ """
22
+
23
+ GUIDE = "guide"
24
+ INLINE = "inline"
25
+ EXAMPLE_ONLY = "exampleOnly"
26
+
27
+
28
+ def prompt_style_from(s: str | None) -> PromptStyle:
29
+ """Map the ``@promptStyle`` attribute string to a :class:`PromptStyle`.
30
+
31
+ ``None`` or any unrecognized value falls back to ``GUIDE`` (matches Java
32
+ ``PromptStyle.from``).
33
+ """
34
+ if s == PromptStyle.INLINE.value:
35
+ return PromptStyle.INLINE
36
+ if s == PromptStyle.EXAMPLE_ONLY.value:
37
+ return PromptStyle.EXAMPLE_ONLY
38
+ return PromptStyle.GUIDE