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,108 @@
1
+ # Java parser-on-receipt
2
+
3
+ For every `template.output`, `codegen-spring`'s `SpringOutputParserGenerator` emits
4
+ a **typed parser** that validates an LLM/raw response against the template's
5
+ `@payloadRef` payload record. This is the receive side only — codegen emits **no**
6
+ provider/LLM-call layer; you compose the call yourself.
7
+
8
+ ## Contents
9
+ - Wire the generator
10
+ - What it emits
11
+ - The three-step consumer pattern
12
+ - Recommended LLM caller (bring-your-own)
13
+ - Drift gate
14
+
15
+ ## Wire the generator
16
+
17
+ Add `SpringOutputParserGenerator` (alongside `SpringPayloadGenerator`, which emits
18
+ the payload record it parses into) to the Maven plugin's `<generators>` list:
19
+
20
+ ```xml
21
+ <generator>
22
+ <classname>com.metaobjects.generator.spring.SpringPayloadGenerator</classname>
23
+ <args><outputDir>${project.build.directory}/generated-sources/java</outputDir></args>
24
+ </generator>
25
+ <generator>
26
+ <classname>com.metaobjects.generator.spring.SpringOutputParserGenerator</classname>
27
+ <args><outputDir>${project.build.directory}/generated-sources/java</outputDir></args>
28
+ </generator>
29
+ ```
30
+
31
+ ## What it emits
32
+
33
+ Per `template.output`, `mvn metaobjects:generate` writes a `<Name>Parser` class with a static
34
+ `parse` method returning the `@payloadRef` payload record. The strict path throws
35
+ `com.fasterxml.jackson.core.JsonProcessingException` on malformed input:
36
+
37
+ ```java
38
+ // generated <Name>Parser.java (shape)
39
+ public final class NpcResponseParser {
40
+ private NpcResponseParser() { } // no instances
41
+
42
+ public static NpcResponsePayload parse(String text) throws JsonProcessingException {
43
+ // Jackson-backed: validates the text against the payload record
44
+ }
45
+ }
46
+ ```
47
+
48
+ For `@format: json|xml` outputs the generator also emits a tolerant best-effort
49
+ variant (`extractLenient(...)` returning an `ExtractionResult<NpcResponsePayload>`
50
+ from `com.metaobjects.render.extract`) for cases where you want classification
51
+ rather than a throw. The payload record itself comes from `SpringPayloadGenerator`
52
+ — the parser is a companion to it, so the parser and payload VO can't silently
53
+ drift.
54
+
55
+ ## The three-step consumer pattern
56
+
57
+ Render the prompt → call your LLM client (provider-agnostic; nothing is generated
58
+ here) → parse the response with the generated parser:
59
+
60
+ ```java
61
+ String promptText = Renderer.render(/* template ref + payload + provider */);
62
+ String llmResponse = myLlmClient.call(promptText); // YOUR code — no generated provider
63
+
64
+ NpcResponsePayload npc = NpcResponseParser.parse(llmResponse); // throws on bad shape
65
+ ```
66
+
67
+ ## Recommended LLM caller (bring-your-own)
68
+
69
+ `codegen-spring` emits **no** provider/LLM-call layer and never will — calling is a
70
+ commodity the ecosystem already solves, and a maintained vendor wrapper would chase
71
+ SDK churn (ADR-0024). You bring the caller; MetaObjects owns the typed render →
72
+ parse (above) → record. For the call step use the idiomatic JVM library:
73
+
74
+ ```java
75
+ // Spring AI (recommended) — provider-agnostic ChatClient; YOUR code, no generated provider
76
+ String response = chatClient.prompt()
77
+ .system(systemText)
78
+ .user(promptText)
79
+ .call()
80
+ .content();
81
+
82
+ NpcResponsePayload npc = NpcResponseParser.parse(response); // the generated parser, above
83
+ ```
84
+
85
+ **Recommended: Spring AI** (`ChatClient`) — Spring-native, provider-agnostic
86
+ (Anthropic / OpenAI / Azure / Bedrock / Ollama), fits an OMDB + Spring-tx app.
87
+ Non-Spring JVM apps: **LangChain4j** (`ChatLanguageModel.generate(prompt)`) is the
88
+ equivalent one-call seam.
89
+
90
+ > The typed-trace recorder + render→call→record convenience loop ship in TypeScript
91
+ > today; the JVM port is planned (ADR-0024). Until then the call is your code and the
92
+ > generated parser is the typed receive side.
93
+
94
+ ## Drift gate
95
+
96
+ The render module's static `Verify.check(String templateText, List<PayloadField> fields, VerifyOptions options)`
97
+ walks a Mustache template's tokens against a payload field tree and returns a
98
+ `List<VerifyError>` — each `{{...}}` reference that doesn't resolve against the
99
+ payload fields yields an error (empty list = no drift). `VerifyOptions.empty()`
100
+ supplies a fully-defaulted options record; `VerifyOptions(provider, requiredSlots,
101
+ requiredTags)` adds partial resolution and required-slot/-tag checks. For the
102
+ output-format fragment specifically, `Verify.checkOutputPrompt(String fragment,
103
+ List<String> requiredFieldNames)` returns a `List<VerifyError>` for every required
104
+ field name absent from the rendered fragment. Assert the returned list is empty in a
105
+ JUnit test in the Maven `test` phase to fail the build on prompt/payload drift. The
106
+ `metaobjects:verify` codegen-drift goal additionally catches a stale committed
107
+ parser. The emitted parser imports Jackson (`com.fasterxml.jackson`), normally
108
+ already on a Spring Boot classpath.
@@ -0,0 +1,130 @@
1
+ # Kotlin parser-on-receipt
2
+
3
+ For every `template.output`, `codegen-kotlin`'s `KotlinOutputParserGenerator` emits
4
+ a **typed parser** that validates an LLM/raw response against the template's
5
+ `@payloadRef` payload data class. This is the receive side only — codegen emits
6
+ **no** provider/LLM-call layer; you compose the call yourself. The payload data
7
+ class itself comes from `KotlinPayloadGenerator` (a `@Serializable data class`), so
8
+ the parser and the payload VO can't silently drift.
9
+
10
+ ## Contents
11
+ - Wire the generators
12
+ - What it emits
13
+ - The three-step consumer pattern
14
+ - Consumer dependency
15
+ - Recommended LLM caller (bring-your-own)
16
+ - Drift gate
17
+
18
+ ## Wire the generators
19
+
20
+ Add `KotlinOutputParserGenerator` (alongside `KotlinPayloadGenerator`, which emits
21
+ the payload it parses into) to the Maven plugin's `<generators>` list:
22
+
23
+ ```xml
24
+ <generator>
25
+ <classname>com.metaobjects.generator.kotlin.KotlinPayloadGenerator</classname>
26
+ <args><outputDir>${project.build.directory}/generated-sources/kotlin</outputDir></args>
27
+ </generator>
28
+ <generator>
29
+ <classname>com.metaobjects.generator.kotlin.KotlinOutputParserGenerator</classname>
30
+ <args><outputDir>${project.build.directory}/generated-sources/kotlin</outputDir></args>
31
+ </generator>
32
+ ```
33
+
34
+ ## What it emits
35
+
36
+ Per `template.output`, `mvn metaobjects:generate` writes a `<Name>Parser.kt`
37
+ `object` with a dual API matching kotlinx.serialization's exception model plus the
38
+ Kotlin stdlib `Result<T>` convention:
39
+
40
+ ```kotlin
41
+ // generated <Name>Parser.kt (shape)
42
+ object NpcResponseParser {
43
+ private val json: Json = Json { ignoreUnknownKeys = false }
44
+
45
+ /** Throws kotlinx.serialization.SerializationException on bad input. */
46
+ fun parseNpcResponse(text: String): NpcResponsePayload =
47
+ json.decodeFromString<NpcResponsePayload>(text)
48
+
49
+ /** Result-style — does not throw. */
50
+ fun safeParseNpcResponse(text: String): Result<NpcResponsePayload> =
51
+ runCatching { parseNpcResponse(text) }
52
+ }
53
+ ```
54
+
55
+ For `@format: json|xml` outputs the generator additionally emits a **tolerant**
56
+ best-effort variant — `extractLenient(...)` returning an
57
+ `ExtractionResult<NpcResponseExtracted>` (from `com.metaobjects.render.extract`) for
58
+ cases where you want a classified per-field report rather than a throw. There are
59
+ two overloads: a self-contained one (scalars/enums only; nested components stay
60
+ null) and a `extractLenient(loader, text)` overload that delegates to the runtime
61
+ `MetaObjectExtractor` to fully populate nested-object and array-of-object
62
+ components. The lenient mirror type (`<Name>Extracted`) uses nullable fields per
63
+ the Kotlin null-safety port — a missing/malformed component is `null`, not a throw.
64
+
65
+ ## The three-step consumer pattern
66
+
67
+ Render the prompt → call your LLM client (provider-agnostic; nothing is generated
68
+ here) → parse the response with the generated parser:
69
+
70
+ ```kotlin
71
+ val response: String = myLlmClient.complete(promptText) // YOUR code — no generated provider
72
+
73
+ // Throwing path — propagate to your error handler
74
+ val npc = NpcResponseParser.parseNpcResponse(response)
75
+
76
+ // Or Result-style
77
+ NpcResponseParser.safeParseNpcResponse(response)
78
+ .onSuccess { npc -> /* use it */ }
79
+ .onFailure { ex -> log.warn("LLM returned malformed payload", ex) }
80
+ ```
81
+
82
+ ## Consumer dependency
83
+
84
+ The emitted parser imports `kotlinx.serialization.json.Json` and calls
85
+ `Json.decodeFromString<T>(text)`. The `kotlinx-serialization-core` artifact alone
86
+ (which `@Serializable` needs) does NOT include the JSON format — add the JSON
87
+ artifact + the serialization plugin:
88
+
89
+ ```kotlin
90
+ plugins { kotlin("plugin.serialization") version "1.9.x" }
91
+ dependencies {
92
+ implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.x")
93
+ }
94
+ ```
95
+
96
+ ## Recommended LLM caller (bring-your-own)
97
+
98
+ `codegen-kotlin` emits **no** provider/LLM-call layer and never will — calling is a
99
+ commodity the ecosystem already solves (ADR-0024). You bring the caller; MetaObjects
100
+ owns the typed render → parse (above) → record. For the call step use the idiomatic
101
+ JVM library — Kotlin calls either directly:
102
+
103
+ ```kotlin
104
+ // Spring AI (recommended) — provider-agnostic ChatClient; YOUR code, no generated provider
105
+ val response: String = chatClient.prompt()
106
+ .system(systemText)
107
+ .user(promptText)
108
+ .call()
109
+ .content()
110
+
111
+ val npc = NpcResponseParser.parseNpcResponse(response) // the generated parser, above
112
+ ```
113
+
114
+ **Recommended: Spring AI** (`ChatClient`) for a Spring app, or **LangChain4j**
115
+ (`ChatLanguageModel.generate(prompt)`) for non-Spring JVM — both provider-agnostic,
116
+ both a one-call seam Kotlin uses idiomatically.
117
+
118
+ > The typed-trace recorder + render→call→record convenience loop ship in TypeScript
119
+ > today; the JVM port is planned (ADR-0024). Until then the call is your code and the
120
+ > generated parser is the typed receive side.
121
+
122
+ ## Drift gate
123
+
124
+ The render engine is the Java `metaobjects-render` module (Kotlin wraps it). Its
125
+ static `Verify.check(...)` walks a Mustache template's tokens against the payload
126
+ field tree and returns a list of errors for any `{{...}}` reference that doesn't
127
+ resolve against the payload — empty list = no drift. Assert it is empty in a JUnit
128
+ test in the Maven `test` phase to fail the build on prompt/payload drift. The
129
+ `metaobjects:verify` codegen-drift goal additionally catches a stale committed
130
+ parser.
@@ -0,0 +1,116 @@
1
+ # Python parser-on-receipt
2
+
3
+ For every `template.output`, the `output-parser` generator (run via `metaobjects gen`)
4
+ emits a **typed parser** that validates an LLM/raw response against the template's
5
+ `@payloadRef` payload Pydantic model. This is the receive side only — codegen emits
6
+ **no** provider/LLM-call layer; you compose the call yourself. The payload class comes
7
+ from the sibling `payload` generator, so the parser and the payload VO can't silently
8
+ drift.
9
+
10
+ ## Contents
11
+ - Wire the generators
12
+ - What it emits
13
+ - The three-step consumer pattern
14
+ - Recommended LLM caller (bring-your-own)
15
+ - Consumer dependency
16
+ - Drift gate
17
+
18
+ ## Wire the generators
19
+
20
+ Select `output-parser` (the `payload` generator that emits the `<Name>Payload` it
21
+ parses into runs alongside it):
22
+
23
+ ```bash
24
+ metaobjects gen ./metadata --out ./generated --generators payload,output-parser
25
+ ```
26
+
27
+ `metaobjects gen --list` shows every generator name; the programmatic
28
+ `run_gen(..., generators=[...])` path takes the same names.
29
+
30
+ ## What it emits
31
+
32
+ Per `template.output`, `metaobjects gen` writes one `<template_name>_output_parser.py`
33
+ with a single throw-only entry point. Python uses one API (not TS's
34
+ `parse`/`safeParse`) because raising `pydantic.ValidationError` is the idiomatic
35
+ failure — the pydantic / Instructor / FastAPI norm; a Result-style wrapper would be
36
+ un-Pythonic.
37
+
38
+ ```python
39
+ # generated <template_name>_output_parser.py (shape)
40
+ from .npc_response_payload import NpcResponsePayload # the @payloadRef VO (Pydantic v2 BaseModel)
41
+
42
+ def parse_npc_response(text: str) -> NpcResponsePayload:
43
+ """Validates text against the payload model.
44
+
45
+ Raises:
46
+ pydantic.ValidationError: when the input does not match the schema.
47
+ """
48
+ ...
49
+ ```
50
+
51
+ For `@format: json|xml` outputs the generator additionally emits a **tolerant**
52
+ best-effort variant — `extract_lenient_<name>(text) -> ExtractionResult[<Name>PayloadExtracted]`
53
+ (from the `metaobjects` render `extract` engine) for cases where you want a classified
54
+ per-field report rather than a raise. The lenient mirror (`<Name>PayloadExtracted`)
55
+ uses `Optional[...]` fields — a missing/malformed component is `None`, not a raise.
56
+
57
+ ## The three-step consumer pattern
58
+
59
+ Render the prompt → call your LLM client (provider-agnostic; nothing is generated
60
+ here) → parse the response with the generated parser:
61
+
62
+ ```python
63
+ from pydantic import ValidationError
64
+ from .npc_response_output_parser import parse_npc_response
65
+
66
+ text = my_llm_client.complete(prompt_text) # YOUR code — no generated provider
67
+ try:
68
+ npc = parse_npc_response(text) # raises pydantic.ValidationError on bad shape
69
+ except ValidationError as exc:
70
+ log.warning("LLM returned malformed payload: %s", exc)
71
+ ```
72
+
73
+ ## Recommended LLM caller (bring-your-own)
74
+
75
+ `metaobjects gen` emits **no** provider/LLM-call layer and never will — calling is a
76
+ commodity the ecosystem already solves (ADR-0024). You bring the caller; MetaObjects
77
+ owns the typed render → parse (above) → record. For the call step use the idiomatic
78
+ Python library:
79
+
80
+ ```python
81
+ from litellm import completion # LiteLLM — recommended
82
+
83
+ resp = completion(
84
+ model="anthropic/claude-3-5-sonnet",
85
+ messages=[{"role": "system", "content": system_text},
86
+ {"role": "user", "content": prompt_text}],
87
+ )
88
+ text = resp.choices[0].message.content
89
+
90
+ npc = parse_npc_response(text) # the generated parser, above
91
+ ```
92
+
93
+ **Recommended: LiteLLM** — one OpenAI-shaped `completion()` over 100+ providers; the
94
+ raw text feeds straight into MetaObjects' typed parser/extract. If you want the *LLM*
95
+ to enforce the typed shape instead, **Instructor** or **Pydantic-AI** return a
96
+ validated Pydantic model — but that overlaps MetaObjects' own typed `extract`, so pick
97
+ one boundary, not both.
98
+
99
+ > The typed-trace recorder + render→call→record convenience loop ship in TypeScript
100
+ > today; the Python port is planned (ADR-0024). Until then the call is your code and
101
+ > the generated parser is the typed receive side.
102
+
103
+ ## Consumer dependency
104
+
105
+ The generated parser imports `pydantic` (v2 — `model_validate_json`); `pip install
106
+ pydantic`. The tolerant `extract_lenient_*` variant pulls the `metaobjects` render
107
+ `extract` engine, already in the `metaobjects` package.
108
+
109
+ ## Drift gate
110
+
111
+ The render module's `verify(template_text, fields, *, provider=None,
112
+ required_slots=None, required_tags=None) -> list[VerifyError]` walks a Mustache
113
+ template's tokens against the payload field tree — each `{{...}}` reference that
114
+ doesn't resolve yields a `VerifyError` (empty list = no drift). Assert it is empty in a
115
+ pytest test to fail the build on prompt/payload drift; `metaobjects verify`
116
+ additionally catches a stale committed parser.
@@ -0,0 +1,150 @@
1
+ # TypeScript parser-on-receipt
2
+
3
+ For every `template.output` in your metadata, the `outputParser()` generator (from
4
+ `@metaobjectsdev/codegen-ts/generators`) emits a **typed parser** that validates an
5
+ LLM/raw response against the template's `@payloadRef` payload value-object. This is
6
+ the receive side; codegen emits **no** provider/LLM-call layer — you compose the
7
+ call yourself.
8
+
9
+ ## Contents
10
+ - Wire the generator
11
+ - What it emits
12
+ - The three-step consumer pattern
13
+ - Recommended LLM caller (bring-your-own)
14
+ - Drift gate
15
+ - See which fields a template consumes
16
+
17
+ ## Wire the generator
18
+
19
+ ```ts
20
+ // metaobjects.config.ts
21
+ import { defineConfig } from "@metaobjectsdev/cli";
22
+ import { entityFile, queriesFile, barrel, promptRender, outputParser } from "@metaobjectsdev/codegen-ts/generators";
23
+
24
+ export default defineConfig({
25
+ outDir: "src/generated",
26
+ generators: [
27
+ entityFile(), queriesFile(), barrel(),
28
+ promptRender(), // render<Name>() per template.prompt (the send side)
29
+ outputParser(), // parse*/safeParse* per template.output (the receive side)
30
+ ],
31
+ });
32
+ ```
33
+
34
+ `outputParser({ outDir: "src/generated/outputs", target: "default" })` are the
35
+ options.
36
+
37
+ ## What it emits
38
+
39
+ Per `template.output` (say named `NpcResponseOutput`, `@payloadRef:
40
+ "NpcResponsePayload"`), `meta gen` writes a self-contained `NpcResponseOutput.output.ts`
41
+ with a Zod schema + a dual API:
42
+
43
+ ```ts
44
+ import { z } from "zod";
45
+
46
+ const NpcResponseOutputSchema = z.object({
47
+ name: z.string(),
48
+ level: z.number().int(),
49
+ role: z.unknown(), // field.enum is not value-constrained in the strict parser → z.unknown()
50
+ });
51
+
52
+ export type NpcResponseOutputData = z.infer<typeof NpcResponseOutputSchema>;
53
+ export type NpcResponseOutputValidationError = z.ZodError;
54
+
55
+ /** Throws ZodError on bad input. */
56
+ export function parseNpcResponseOutput(text: string): NpcResponseOutputData { /* ... */ }
57
+
58
+ /** Result-style; never throws. */
59
+ export function safeParseNpcResponseOutput(text: string):
60
+ | { success: true; data: NpcResponseOutputData }
61
+ | { success: false; error: NpcResponseOutputValidationError } { /* ... */ }
62
+ ```
63
+
64
+ The dual API mirrors Zod's idiomatic shape: `parse*` throws a `ZodError`,
65
+ `safeParse*` returns a discriminated union. The emitted `<Name>Data` type is
66
+ structurally identical to the `promptRender()` payload VO, so you can pass values
67
+ between the render and parse sides interchangeably.
68
+
69
+ Field-type → Zod mapping: `field.string` → `z.string()`; `field.int`/`long`/`short`/`byte`
70
+ → `z.number().int()`; `field.double`/`float` → `z.number()`; `field.boolean` →
71
+ `z.boolean()`; `field.object` (with `@objectRef`) → a nested `z.object({...})`;
72
+ `isArray: true` → wrapped in `z.array(...)`. Any subtype outside this scalar set —
73
+ including `field.enum` — falls through to `z.unknown()` in the strict
74
+ `parse*`/`safeParse*` schema (the value-constrained `z.enum([...])` form is emitted
75
+ in the entity insert/update schemas, not in this output parser; the lenient extract
76
+ path carries the enum-as-string handling).
77
+
78
+ ## The three-step consumer pattern
79
+
80
+ Render the prompt → call your LLM client (provider-agnostic; nothing is generated
81
+ here) → parse the response with the generated parser:
82
+
83
+ ```ts
84
+ import { renderNpcPrompt } from "./generated/prompts";
85
+ import { parseNpcResponseOutput, safeParseNpcResponseOutput } from "./generated/NpcResponseOutput.output";
86
+
87
+ const promptText = renderNpcPrompt(payload, textProvider);
88
+ const llmResponse = await myLlmProvider.call(promptText); // YOUR code — no generated provider
89
+
90
+ // Throwing path:
91
+ const npc = parseNpcResponseOutput(llmResponse);
92
+
93
+ // Result-style:
94
+ const r = safeParseNpcResponseOutput(llmResponse);
95
+ if (!r.success) log.warn("malformed LLM payload", r.error);
96
+ else handle(r.data);
97
+ ```
98
+
99
+ ## Recommended LLM caller (bring-your-own)
100
+
101
+ MetaObjects generates **no** provider/LLM-call layer and never will — the call is a
102
+ commodity the ecosystem already solves, and a maintained vendor wrapper would just
103
+ chase SDK churn (ADR-0024). You bring the caller; MetaObjects owns the typed
104
+ render → parse → record. For the call step, plug the idiomatic library into the
105
+ one-method `LlmClient` seam from `@metaobjectsdev/ai-runtime`:
106
+
107
+ ```ts
108
+ import { generateText } from "ai"; // Vercel AI SDK — recommended
109
+ import { anthropic } from "@ai-sdk/anthropic";
110
+ import type { LlmClient } from "@metaobjectsdev/ai-runtime";
111
+
112
+ export const llm: LlmClient = {
113
+ async complete({ prompt, model, system }) {
114
+ const { text } = await generateText({ model: anthropic(model), system, prompt });
115
+ return { body: text }; // also map usage/model/finishReason from the result → cost + token columns
116
+ },
117
+ };
118
+ ```
119
+
120
+ **Recommended: the Vercel AI SDK (`ai`)** — provider-agnostic (`generateText` /
121
+ `generateObject` over Anthropic / OpenAI / Google / …), first-class structured
122
+ output, the de-facto TS standard. Single-provider apps can implement `complete`
123
+ directly over `@anthropic-ai/sdk` / `openai`; heavy chains can use LangChain.js —
124
+ the seam is one method either way.
125
+
126
+ With a client in hand, the generated `record<Entity>` helper (the `trace-helper`
127
+ generator) and `@metaobjectsdev/ai-runtime`'s `callLlm` do render → call → **typed
128
+ trace persist** in one call, recording request/response as typed value objects in
129
+ your own DB. The parser above is the standalone receive side if you don't want the
130
+ recorder.
131
+
132
+ ## Drift gate
133
+
134
+ `meta verify` walks every `template.output`'s `@payloadRef` resolution and fails the
135
+ build (exit 1, `(output)`-prefixed diagnostic) if a reference can't be resolved —
136
+ catching payload-VO ↔ parser drift at build time. The emitted parser imports `zod`;
137
+ it's usually already a dependency (Drizzle / `runtime-ts` lean on it), else
138
+ `npm i zod`.
139
+
140
+ ## See which fields a template consumes
141
+
142
+ Run `meta docs` to emit the model surface to `./docs` — one page per
143
+ `template.output` at `docs/<Template>.md`. Each template page has a
144
+ `## Template source` section that shows the Mustache source with every `{{var}}`
145
+ linked to that field's doc page (`docs/<Owner>.md#field-<name>`), plus a variables
146
+ table — so you can see exactly which payload fields a template reads. Those links are
147
+ build-time drift-gated against the render `verify()` engine, so a link can't claim a
148
+ field that `verify()` would reject. (`meta docs` is the single door for all docs —
149
+ it's a command, not a `meta gen` generator; the model surface here lands in `./docs`,
150
+ the SDK/API surface under `./docs/api`.)
@@ -0,0 +1,130 @@
1
+ ---
2
+ name: metaobjects-runtime-ui
3
+ description: Use when wiring MetaObjects generated code into an app: runtime queries/CRUD, REST routes, and the web client (forms, grids, filters).
4
+ ---
5
+
6
+ # Wiring MetaObjects runtime + web client
7
+
8
+ This skill is the procedure for putting generated code to work: querying/persisting
9
+ through the runtime tier, serving the REST contract, and consuming it from the
10
+ universal web client. It is port-agnostic — the cross-cutting contracts are here;
11
+ the server- and client-specific imports/types live in reference fragments (pointed
12
+ to at the bottom).
13
+
14
+ ## The runtime return-type contract
15
+
16
+ A port's runtime returns **native in-process language types**, never wire strings.
17
+ Query the runtime and you get back the language's natural type for each field:
18
+
19
+ - `field.decimal` → the language's exact-decimal type (`BigDecimal` / `decimal` /
20
+ `Decimal`). Decimal is **lossless end-to-end** — no float round-tripping.
21
+ - temporal fields → native date/instant types.
22
+ - `field.object` (jsonb) → a native map/object.
23
+ - **TypeScript is the documented outlier: `field.decimal` is a `string`** in the
24
+ TS runtime (JS has no native exact decimal), preserving precision as text.
25
+
26
+ Wire canonicalization (integer minor units for currency, ISO-8601 strings for
27
+ temporals, canonical hex for UUID) is applied only at the **serialization
28
+ boundary** — when a row leaves over HTTP — never inside the runtime query path. So:
29
+ compute with native types in-process; the HTTP layer handles wire encoding.
30
+
31
+ ## Generated repo / query helpers take the context as a parameter
32
+
33
+ Generated finders and CRUD helpers **do not** reach for a module-level `db`
34
+ singleton. They take the persistence context (connection / session / data-access
35
+ handle) as an explicit **parameter**. You own the connection's lifecycle and pass
36
+ it in:
37
+
38
+ ```
39
+ const rows = await findPostsForAuthor(db, authorId); // db is passed, not imported
40
+ ```
41
+
42
+ This keeps generated code free of global state, makes it testable, and lets one
43
+ process talk to multiple databases (multi-tenant, read-replica). Construct/own the
44
+ context in your app; thread it through every generated call.
45
+
46
+ ## The REST contract
47
+
48
+ Generated (or hand-written) routes speak one cross-port HTTP contract so the same
49
+ universal web client serves any backend language.
50
+
51
+ ### URL grammar
52
+
53
+ `apiPrefix` (default `/api`, set in project config) flows to both the server routes
54
+ and the client fetch URLs. `<entity>` is lowercased + pluralized (`Author` →
55
+ `authors`).
56
+
57
+ | Verb | Path | Purpose |
58
+ |---|---|---|
59
+ | `GET` | `/<apiPrefix>/<entity>?filter[...][...]=...&sort=...&limit=N&offset=N&withCount=1` | List |
60
+ | `GET` | `/<apiPrefix>/<entity>/:id` | Get by id |
61
+ | `POST` | `/<apiPrefix>/<entity>` | Create (201) |
62
+ | `PATCH` | `/<apiPrefix>/<entity>/:id` | Update (partial) |
63
+ | `PUT` | `/<apiPrefix>/<entity>/:id` | Update (replace) — optional |
64
+ | `DELETE` | `/<apiPrefix>/<entity>/:id` | Delete (204) |
65
+
66
+ ### Filter operators by field subtype
67
+
68
+ Filters use a bracketed qs: `filter[<field>][<op>]=<value>`. A bare
69
+ `filter[<field>]=<value>` is sugar for `eq`. The operator set is **gated by field
70
+ subtype** via the generated `<Entity>FilterAllowlist` — an unsupported operator for
71
+ a field → HTTP 400.
72
+
73
+ | Operator | Strings | Numbers / Dates | Booleans |
74
+ |---|---|---|---|
75
+ | `eq`, `ne`, `isNull` | yes | yes | `eq` + `isNull` only |
76
+ | `in`, `like` | yes | `in` only | – |
77
+ | `gt`, `gte`, `lt`, `lte` | – | yes | – |
78
+
79
+ These eight (`eq` `ne` `gt` `gte` `lt` `lte` `in` `like` `isNull`) are the whole
80
+ closed set — every port implements these and only these.
81
+
82
+ ### Sort + pagination
83
+
84
+ - `sort=<field>:asc|desc` — single sort key; the field must be in
85
+ `<Entity>SortAllowlist`.
86
+ - `limit=N` / `offset=N` — page size + offset, identical across every endpoint.
87
+ - `withCount=1` — switches the list response from `[<row>...]` to
88
+ `{ rows: [...], total: N }` (grids always send it).
89
+
90
+ ### Wire format
91
+
92
+ JSON bodies (`application/json; charset=utf-8`). Single-row responses (`GET /:id`,
93
+ `POST`, `PATCH`, `PUT`) have **no envelope** — the body is the row. Type encodings:
94
+
95
+ | Field type | JSON | Notes |
96
+ |---|---|---|
97
+ | `field.string` / `field.uuid` / `field.enum` | string | UUID is canonical hex `8-4-4-4-12` |
98
+ | `field.int` / `field.long` / `field.double` | number | `long` may be string on overflow (per-port) |
99
+ | `field.boolean` | boolean | |
100
+ | `field.date` | string | ISO 8601 `YYYY-MM-DD` |
101
+ | `field.timestamp` | string | ISO 8601 with timezone |
102
+ | `field.currency` | **integer minor units** | cents/yen; float arithmetic forbidden; server never formats |
103
+ | `field.object` | object | per the sub-object schema |
104
+
105
+ The currency invariant is load-bearing: integer minor units on the wire, always.
106
+ Formatting happens client-side with locale-aware code.
107
+
108
+ Errors: non-2xx returns `{ "error": "<short_code>", "message"?: "..." }`. 400 for
109
+ validation/filter-parser errors, 404 for not-found (`{ "error": "not_found" }`),
110
+ 5xx implementation-defined. Treat any 4xx as user-facing, any 5xx as retryable.
111
+
112
+ ## The EntityFetcher browser contract
113
+
114
+ The web client never calls `fetch` directly. Every generated hook delegates to a
115
+ single `EntityFetcher` you supply once at the app root:
116
+
117
+ ```ts
118
+ export type EntityFetcher = <T>(path: string, init?: RequestInit) => Promise<T>;
119
+ ```
120
+
121
+ Your fetcher resolves the `path` (always starting with `apiPrefix`) to a full URL,
122
+ attaches auth (cookie / bearer / API key) per your policy, parses the JSON, and
123
+ throws on non-2xx (the hooks rely on the throw for error state). Provide it via the
124
+ fetcher-provider at the tree root; every generated hook reads it from context. The
125
+ generated grid and form components, filter-qs serializer, and cell renderers all
126
+ sit on top of this one seam.
127
+
128
+ ---
129
+
130
+ For this project's runtime + web-client specifics, read every `references/*.md` file in this skill's directory (one per server language and client framework in this project's stack).